diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index f814ffabf..f241abd25 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -420,22 +420,21 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { account.update(articles, statusKey: statusKey, flag: flag) { result in - switch result { - case .success(let articles): - let syncStatuses = articles.map { article in - return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) - } - - self.database.insertStatuses(syncStatuses) { _ in - self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { - self.sendArticleStatus(for: account, showProgress: false) { _ in } - } - completion(.success(())) + Task { @MainActor in + switch result { + case .success(let articles): + let syncStatuses = articles.map { article in + return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) } + + try? await self.database.insertStatuses(syncStatuses) + if let count = try? await self.database.selectPendingCount(), count > 100 { + self.sendArticleStatus(for: account, showProgress: false) { _ in } + } + completion(.success(())) + case .failure(let error): + completion(.failure(error)) } - case .failure(let error): - completion(.failure(error)) } } } @@ -446,24 +445,25 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress, articlesZone: articlesZone) articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone) - - database.resetAllSelectedForProcessing() - - // Check to see if this is a new account and initialize anything we need - if account.externalID == nil { - accountZone.findOrCreateAccount() { [weak self] result in - switch result { - case .success(let externalID): - account.externalID = externalID - self?.initialRefreshAll(for: account) { _ in } - case .failure(let error): - self?.logger.error("Error adding account container: \(error.localizedDescription, privacy: .public)") + + Task { @MainActor in + try? await database.resetAllSelectedForProcessing() + + // Check to see if this is a new account and initialize anything we need + if account.externalID == nil { + accountZone.findOrCreateAccount() { [weak self] result in + switch result { + case .success(let externalID): + account.externalID = externalID + self?.initialRefreshAll(for: account) { _ in } + case .failure(let error): + self?.logger.error("Error adding account container: \(error.localizedDescription, privacy: .public)") + } } + accountZone.subscribeToZoneChanges() + articlesZone.subscribeToZoneChanges() } - accountZone.subscribeToZoneChanges() - articlesZone.subscribeToZoneChanges() } - } func accountWillBeDeleted(_ account: Account) { @@ -773,7 +773,8 @@ private extension CloudKitAccountDelegate { let syncStatuses = articles.map { article in return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) } - database.insertStatuses(syncStatuses) { _ in + Task { @MainActor in + try? await database.insertStatuses(syncStatuses) completion() } } diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index d20959df4..a86568a25 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -30,36 +30,30 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { - database.selectPendingReadStatusArticleIDs() { result in - switch result { - case .success(let pendingReadStatusArticleIDs): + Task { @MainActor in - self.database.selectPendingStarredStatusArticleIDs() { result in - switch result { - case .success(let pendingStarredStatusArticleIDs): - self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in - Task { @MainActor in - if let error = error { - completion(.failure(error)) - } else { - self.update(records: updated, - pendingReadStatusArticleIDs: pendingReadStatusArticleIDs, - pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs, - completion: completion) - } - } - } - case .failure(let error): - self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)") - completion(.failure(CloudKitZoneError.unknown)) - } - } - case .failure(let error): - self.logger.error("Error occurred getting pending read status records: \(error.localizedDescription, privacy: .public)") - completion(.failure(CloudKitZoneError.unknown)) - } - } - } + do { + let pendingReadStatusArticleIDs = try await self.database.selectPendingReadArticleIDs() + let pendingStarredStatusArticleIDs = try await self.database.selectPendingStarredArticleIDs() + + self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in + Task { @MainActor in + if let error = error { + completion(.failure(error)) + } else { + self.update(records: updated, + pendingReadStatusArticleIDs: pendingReadStatusArticleIDs, + pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs, + completion: completion) + } + } + } + } catch { + self.logger.error("Error occurred getting pending read status records: \(error.localizedDescription, privacy: .public)") + completion(.failure(CloudKitZoneError.unknown)) + } + } + } } private extension CloudKitArticlesZoneDelegate { @@ -73,23 +67,17 @@ private extension CloudKitArticlesZoneDelegate { completion(nil) return } - - database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in - Task { @MainActor in - if let databaseError = databaseError { - completion(databaseError) - return - } - - do { - try await self.account?.deleteArticleIDs(deletableArticleIDs) - completion(nil) - } catch let error { - completion(error) - } - } - } - } + + Task { @MainActor in + do { + try await database.deleteSelectedForProcessing(Array(deletableArticleIDs)) + try await self.account?.deleteArticleIDs(deletableArticleIDs) + completion(nil) + } catch { + completion(error) + } + } + } @MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set, pendingStarredStatusArticleIDs: Set, completion: @escaping (Result) -> Void) { @@ -147,30 +135,30 @@ private extension CloudKitArticlesZoneDelegate { let parsedItems = records.compactMap { self.makeParsedItem($0) } let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) } - DispatchQueue.main.async { + Task { @MainActor in for (feedID, parsedItems) in feedIDsAndItems { group.enter() self.account?.update(feedID, with: parsedItems, deleteOlder: false) { result in - switch result { - case .success(let articleChanges): - guard let deletes = articleChanges.deletedArticles, !deletes.isEmpty else { + Task { @MainActor in + switch result { + case .success(let articleChanges): + guard let deletes = articleChanges.deletedArticles, !deletes.isEmpty else { + group.leave() + return + } + let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) } + try? await self.database.insertStatuses(syncStatuses) group.leave() - return - } - let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) } - self.database.insertStatuses(syncStatuses) { _ in + case .failure(let databaseError): + errorOccurred = true + self.logger.error("Error occurred while storing articles: \(databaseError.localizedDescription, privacy: .public)") group.leave() } - case .failure(let databaseError): - errorOccurred = true - self.logger.error("Error occurred while storing articles: \(databaseError.localizedDescription, privacy: .public)") - group.leave() } } } group.leave() } - } group.notify(queue: DispatchQueue.main) { diff --git a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index 5579018c9..3d65891df 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -38,62 +38,56 @@ class CloudKitSendStatusOperation: MainThreadOperation, Logging { } func run() { - logger.debug("Sending article statuses...") - - if showProgress { - - database.selectPendingCount() { result in - switch result { - case .success(let count): + + Task { @MainActor in + logger.debug("Sending article statuses...") + + if showProgress { + do { + let count = try await database.selectPendingCount() let ticks = count / self.blockSize self.refreshProgress?.addToNumberOfTasksAndRemaining(ticks) self.selectForProcessing() - case .failure(let databaseError): - self.logger.error("Send status count pending error: \(databaseError.localizedDescription, privacy: .public)") + } catch { + self.logger.error("Send status count pending error: \(error.localizedDescription, privacy: .public)") self.operationDelegate?.cancelOperation(self) } + } else { + selectForProcessing() } - - } else { - - selectForProcessing() - } - } - } private extension CloudKitSendStatusOperation { func selectForProcessing() { - database.selectForProcessing(limit: blockSize) { result in - switch result { - case .success(let syncStatuses): - - @MainActor func stopProcessing() { - if self.showProgress { - self.refreshProgress?.completeTask() - } - self.logger.debug("Done sending article statuses.") - self.operationDelegate?.operationDidComplete(self) - } - + + @MainActor func stopProcessing() { + if self.showProgress { + self.refreshProgress?.completeTask() + } + self.logger.debug("Done sending article statuses.") + self.operationDelegate?.operationDidComplete(self) + } + + Task { @MainActor in + do { + let syncStatuses = try await database.selectForProcessing(limit: blockSize) guard syncStatuses.count > 0 else { stopProcessing() return } - - self.processStatuses(syncStatuses) { stop in + + self.processStatuses(Array(syncStatuses)) { stop in if stop { stopProcessing() } else { self.selectForProcessing() } } - - case .failure(let databaseError): - self.logger.error("Send status error: \(databaseError.localizedDescription, privacy: .public)") + } catch { + self.logger.error("Send status error: \(error.localizedDescription, privacy: .public)") self.operationDelegate?.cancelOperation(self) } } @@ -128,42 +122,42 @@ private extension CloudKitSendStatusOperation { } // If this happens, we have somehow gotten into a state where we have new status records - // but the articles didn't come back in the fetch. We need to clean up those sync records + // but the articles didn't come back in the fetch. We need to clean up those sync records // and stop processing. if statusUpdates.isEmpty { - self.database.deleteSelectedForProcessing(articleIDs) { _ in + Task { @MainActor in + try? await self.database.deleteSelectedForProcessing(articleIDs) done(true) - return } + return } else { articlesZone.modifyArticles(statusUpdates) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) { _ in + Task { @MainActor in + switch result { + case .success: + try? await self.database.deleteSelectedForProcessing(statusUpdates.map({ $0.articleID })) done(false) - } - case .failure(let error): - self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in + case .failure(let error): + try? await self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) self.processAccountError(account, error) - self.logger.error("Send article status modify articles error: \(error.localizedDescription, privacy: .public)") + self.logger.error("Send article status modify articles error: \(error.localizedDescription, privacy: .public)") completion(true) } } } } - } switch result { case .success(let articles): processWithArticles(articles) case .failure(let databaseError): - self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in - self.logger.error("Send article status fetch articles error: \(databaseError.localizedDescription, privacy: .public)") + Task { @MainActor in + try? await self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) + self.logger.error("Send article status fetch articles error: \(databaseError.localizedDescription, privacy: .public)") completion(true) } } - } } diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift index 85d8c0453..7ba4ae4fe 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift @@ -130,11 +130,12 @@ public enum FeedbinAccountDelegateError: String, Error { func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - logger.debug("Sending article statuses") + Task { @MainActor in + logger.debug("Sending article statuses") - database.selectForProcessing { result in + do { + let syncStatuses = try await self.database.selectForProcessing() - @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } @@ -144,7 +145,7 @@ public enum FeedbinAccountDelegateError: String, Error { var errorOccurred = false group.enter() - self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in + self.sendArticleStatuses(Array(createUnreadStatuses), apiCall: self.caller.createUnreadEntries) { result in group.leave() if case .failure = result { errorOccurred = true @@ -152,7 +153,15 @@ public enum FeedbinAccountDelegateError: String, Error { } group.enter() - self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in + self.sendArticleStatuses(Array(deleteUnreadStatuses), apiCall: self.caller.deleteUnreadEntries) { result in + group.leave() + if case .failure = result { + errorOccurred = true + } + } + + group.enter() + self.sendArticleStatuses(Array(createStarredStatuses), apiCall: self.caller.createStarredEntries) { result in group.leave() if case .failure = result { errorOccurred = true @@ -160,15 +169,7 @@ public enum FeedbinAccountDelegateError: String, Error { } group.enter() - self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in - group.leave() - if case .failure = result { - errorOccurred = true - } - } - - group.enter() - self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in + self.sendArticleStatuses(Array(deleteStarredStatuses), apiCall: self.caller.deleteStarredEntries) { result in group.leave() if case .failure = result { errorOccurred = true @@ -176,20 +177,15 @@ public enum FeedbinAccountDelegateError: String, Error { } group.notify(queue: DispatchQueue.main) { - self.logger.debug("Done sending article statuses.") + self.logger.debug("Done sending article statuses.") if errorOccurred { completion(.failure(FeedbinAccountDelegateError.unknown)) } else { completion(.success(())) } } - } - - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) + } catch { + completion(.failure(error)) } } } @@ -590,15 +586,20 @@ public enum FeedbinAccountDelegateError: String, Error { let syncStatuses = articles.map { article in return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) } - - self.database.insertStatuses(syncStatuses) { _ in - self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { + + Task { @MainActor in + do { + try await self.database.insertStatuses(syncStatuses) + let count = try await self.database.selectPendingCount() + if count > 100 { self.sendArticleStatus(for: account) { _ in } } completion(.success(())) + } catch { + completion(.failure(error)) } } + case .failure(let error): completion(.failure(error)) } @@ -985,18 +986,19 @@ private extension FeedbinAccountDelegate { group.enter() apiCall(articleIDGroup) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } ) - group.leave() - case .failure(let error): - errorOccurred = true - self.logger.error("Article status sync call failed: \(error.localizedDescription, privacy: .public)") - self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } ) - group.leave() + Task { @MainActor in + switch result { + case .success: + try? await self.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } ) + group.leave() + case .failure(let error): + errorOccurred = true + self.logger.error("Article status sync call failed: \(error.localizedDescription, privacy: .public)") + try? await self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } ) + group.leave() + } } } - } group.notify(queue: DispatchQueue.main) { @@ -1006,7 +1008,6 @@ private extension FeedbinAccountDelegate { completion(.success(())) } } - } func renameFolderRelationship(for account: Account, fromName: String, toName: String) { diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 0b62c58ab..368698ccd 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -535,22 +535,27 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { account.update(articles, statusKey: statusKey, flag: flag) { result in - switch result { - case .success(let articles): - let syncStatuses = articles.map { article in - return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) - } - self.database.insertStatuses(syncStatuses) { _ in - self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { + Task { @MainActor in + switch result { + case .success(let articles): + let syncStatuses = articles.map { article in + return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) + } + + do { + try await self.database.insertStatuses(syncStatuses) + let count = try await self.database.selectPendingCount() + if count > 100 { self.sendArticleStatus(for: account) { _ in } } completion(.success(())) + } catch { + completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } - case .failure(let error): - completion(.failure(error)) } } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift index df3d37664..6aef1da75 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -75,14 +75,12 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation, Logging { return } - database.selectPendingStarredStatusArticleIDs { result in - switch result { - case .success(let pendingArticleIds): - self.remoteEntryIds.subtract(pendingArticleIds) - + Task { @MainActor in + do { + let pendingArticleIDs = try await database.selectPendingStarredArticleIDs() + self.remoteEntryIds.subtract(pendingArticleIDs) self.updateStarredStatuses() - - case .failure(let error): + } catch { self.didFinish(with: error) } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift index e267d1b17..5406beced 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -75,15 +75,13 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation, Logging { didFinish() return } - - database.selectPendingReadStatusArticleIDs { result in - switch result { - case .success(let pendingArticleIDs): + + Task { @MainActor in + do { + let pendingArticleIDs = try await database.selectPendingReadArticleIDs() self.remoteEntryIDs.subtract(pendingArticleIDs) - self.updateUnreadStatuses() - - case .failure(let error): + } catch { self.didFinish(with: error) } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift index 6c1383537..dfa69095d 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift @@ -24,18 +24,13 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation, Logging { } override func run() { - logger.debug("Sending article statuses...") + Task { @MainActor in + logger.debug("Sending article statuses...") - database.selectForProcessing { result in - if self.isCanceled { - self.didFinish() - return - } - - switch result { - case .success(let syncStatuses): - self.processStatuses(syncStatuses) - case .failure: + do { + let syncStatuses = try await database.selectForProcessing() + self.processStatuses(Array(syncStatuses)) + } catch { self.didFinish() } } @@ -64,16 +59,14 @@ private extension FeedlySendArticleStatusesOperation { let database = self.database group.enter() service.mark(ids, as: pairing.action) { result in - assert(Thread.isMainThread) - switch result { - case .success: - database.deleteSelectedForProcessing(Array(ids)) { _ in - group.leave() - } - case .failure: - database.resetSelectedForProcessing(Array(ids)) { _ in - group.leave() + Task { @MainActor in + switch result { + case .success: + try? await database.deleteSelectedForProcessing(Array(ids)) + case .failure: + try? await database.resetSelectedForProcessing(Array(ids)) } + group.leave() } } } diff --git a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift b/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift index d50685c45..0614dcb1f 100644 --- a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift +++ b/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift @@ -294,15 +294,25 @@ extension NewsBlurAccountDelegate { for storyHashGroup in storyHashGroups { group.enter() apiCall(storyHashGroup) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(storyHashGroup.map { String($0) } ) - group.leave() - case .failure(let error): - errorOccurred = true - self.logger.error("Story status sync call failed: \(error.localizedDescription, privacy: .public)") - self.database.resetSelectedForProcessing(storyHashGroup.map { String($0) } ) - group.leave() + Task { @MainActor in + let articleIDs = storyHashGroup.map { String($0) } + switch result { + case .success: + do { + try await self.database.deleteSelectedForProcessing(articleIDs) + group.leave() + } catch { + errorOccurred = true + self.logger.error("Story status sync call failed: \(error.localizedDescription, privacy: .public)") + try? await self.database.resetSelectedForProcessing(articleIDs) + group.leave() + } + case .failure(let error): + errorOccurred = true + self.logger.error("Story status sync call failed: \(error.localizedDescription, privacy: .public)") + try? await self.database.resetSelectedForProcessing(articleIDs) + group.leave() + } } } } diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift index e65c46c0e..11aac5837 100644 --- a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -131,10 +131,10 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { - logger.debug("Sending story statuses...") - database.selectForProcessing { result in + Task { @MainActor in + logger.debug("Sending story statuses") - @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } @@ -184,7 +184,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } group.notify(queue: DispatchQueue.main) { - self.logger.debug("Done sending article statuses.") + self.logger.debug("Done sending article statuses.") if errorOccurred { completion(.failure(NewsBlurError.unknown)) } else { @@ -193,11 +193,11 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) + do { + let syncStatuses = try await database.selectForProcessing() + processStatuses(Array(syncStatuses)) + } catch { + completion(.failure(error)) } } } @@ -609,19 +609,25 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { account.update(articles, statusKey: statusKey, flag: flag) { result in switch result { + case .success(let articles): let syncStatuses = articles.map { article in return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) } - self.database.insertStatuses(syncStatuses) { _ in - self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { + Task { @MainActor in + do { + try await self.database.insertStatuses(syncStatuses) + let count = try await self.database.selectPendingCount() + if count > 100 { self.sendArticleStatus(for: account) { _ in } } completion(.success(())) + } catch { + completion(.failure(error)) } } + case .failure(let error): completion(.failure(error)) } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 7f6e7c306..13539d671 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -194,53 +194,52 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - logger.debug("Sending article statuses...") + logger.debug("Sending article statuses") - database.selectForProcessing { result in + @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { + let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } + let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } + let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } + let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } - @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { - let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } - let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } - let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } - let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } + let group = DispatchGroup() - let group = DispatchGroup() - - group.enter() - self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { - group.leave() - } - - group.enter() - self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { - group.leave() - } - - group.enter() - self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { - group.leave() - } - - group.enter() - self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - self.logger.debug("Done sending article statuses.") - completion(.success(())) - } + group.enter() + self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { + group.leave() } - switch result { - case .success(let syncStatuses): - processStatuses(syncStatuses) - case .failure(let databaseError): - completion(.failure(databaseError)) + group.enter() + self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { + group.leave() + } + + group.enter() + self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { + group.leave() + } + + group.enter() + self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + self.logger.debug("Done sending article statuses.") + completion(.success(())) + } + } + + Task { @MainActor in + do { + let syncStatuses = try await database.selectForProcessing() + processStatuses(Array(syncStatuses)) + } catch { + completion(.failure(error)) } } } - + func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Refreshing article statuses...") @@ -640,13 +639,17 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { let syncStatuses = articles.map { article in return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) } - - self.database.insertStatuses(syncStatuses) { _ in - self.database.selectPendingCount { result in - if let count = try? result.get(), count > 100 { + + Task { @MainActor in + do { + try await self.database.insertStatuses(syncStatuses) + let count = try await self.database.selectPendingCount() + if count > 100 { self.sendArticleStatus(for: account) { _ in } } completion(.success(())) + } catch { + completion(.failure(error)) } } case .failure(let error): @@ -910,23 +913,23 @@ private extension ReaderAPIAccountDelegate { group.enter() apiCall(articleIDGroup) { result in - switch result { - case .success: - self.database.deleteSelectedForProcessing(articleIDGroup.map { $0 } ) - group.leave() - case .failure(let error): - self.logger.error("Article status sync call failed: \(error.localizedDescription, privacy: .public)") - self.database.resetSelectedForProcessing(articleIDGroup.map { $0 } ) - group.leave() + Task { @MainActor in + switch result { + case .success: + try? await self.database.deleteSelectedForProcessing(articleIDGroup.map { $0 } ) + group.leave() + case .failure(let error): + self.logger.error("Article status sync call failed: \(error.localizedDescription, privacy: .public)") + try? await self.database.resetSelectedForProcessing(articleIDGroup.map { $0 } ) + group.leave() + } } } - } group.notify(queue: DispatchQueue.main) { completion() } - } func clearFolderRelationship(for feed: Feed, folderExternalID: String?) { diff --git a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift index c0fe7d068..2572bf22a 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift @@ -10,12 +10,6 @@ import Foundation import RSCore import RSDatabase -public typealias SyncStatusesResult = Result, DatabaseError> -public typealias SyncStatusesCompletionBlock = (SyncStatusesResult) -> Void - -public typealias SyncStatusArticleIDsResult = Result, DatabaseError> -public typealias SyncStatusArticleIDsCompletionBlock = (SyncStatusArticleIDsResult) -> Void - public struct SyncDatabase { private let syncStatusTable: SyncStatusTable @@ -32,44 +26,36 @@ public struct SyncDatabase { // MARK: - API - public func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { - syncStatusTable.insertStatuses(statuses, completion: completion) - } - - public func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { - return syncStatusTable.selectForProcessing(limit: limit, completion: completion) + public func insertStatuses(_ statuses: [SyncStatus]) async throws { + try await syncStatusTable.insertStatuses(statuses) } - public func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) { - syncStatusTable.selectPendingCount(completion) + public func selectForProcessing(limit: Int? = nil) async throws -> Set { + try await syncStatusTable.selectForProcessing(limit: limit) + } + + public func selectPendingCount() async throws -> Int { + try await syncStatusTable.selectPendingCount() } public func selectPendingReadArticleIDs() async throws -> Set { try await syncStatusTable.selectPendingReadArticleIDs() } - public func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - syncStatusTable.selectPendingReadStatusArticleIDs(completion: completion) - } - public func selectPendingStarredArticleIDs() async throws -> Set { try await syncStatusTable.selectPendingStarredArticleIDs() } - public func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - syncStatusTable.selectPendingStarredStatusArticleIDs(completion: completion) - } - - public func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { - syncStatusTable.resetAllSelectedForProcessing(completion: completion) + public func resetAllSelectedForProcessing() async throws { + try await syncStatusTable.resetAllSelectedForProcessing() } - public func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { - syncStatusTable.resetSelectedForProcessing(articleIDs, completion: completion) + public func resetSelectedForProcessing(_ articleIDs: [String]) async throws { + try await syncStatusTable.resetSelectedForProcessing(articleIDs) } - - public func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { - syncStatusTable.deleteSelectedForProcessing(articleIDs, completion: completion) + + public func deleteSelectedForProcessing(_ articleIDs: [String]) async throws { + try await syncStatusTable.deleteSelectedForProcessing(articleIDs) } // MARK: - Suspend and Resume (for iOS) diff --git a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift index 560926793..4f0619260 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift @@ -21,67 +21,58 @@ struct SyncStatusTable: DatabaseTable { self.queue = queue } - func selectForProcessing(limit: Int?, completion: @escaping SyncStatusesCompletionBlock) { - queue.runInTransaction { databaseResult in - var statuses = Set() - var error: DatabaseError? + func selectForProcessing(limit: Int?) async throws -> Set { - func makeDatabaseCall(_ database: FMDatabase) { - let updateSQL = "update syncStatus set selected = true" - database.executeUpdate(updateSQL, withArgumentsIn: nil) + return try await withCheckedThrowingContinuation { continuation in + queue.runInTransaction { databaseResult in - var selectSQL = "select * from syncStatus where selected == true" - if let limit = limit { - selectSQL = "\(selectSQL) limit \(limit)" + func makeDatabaseCall(_ database: FMDatabase) -> Set { + let updateSQL = "update syncStatus set selected = true" + database.executeUpdate(updateSQL, withArgumentsIn: nil) + + var selectSQL = "select * from syncStatus where selected == true" + if let limit = limit { + selectSQL = "\(selectSQL) limit \(limit)" + } + + var statuses = Set() + if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) { + statuses = resultSet.mapToSet(self.statusWithRow) + } + return statuses } - if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) { - statuses = resultSet.mapToSet(self.statusWithRow) - } - } - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - case .failure(let databaseError): - error = databaseError - } - - DispatchQueue.main.async { - if let error = error { - completion(.failure(error)) - } - else { - completion(.success(Array(statuses))) + switch databaseResult { + case .success(let database): + let statuses = makeDatabaseCall(database) + continuation.resume(returning: statuses) + case .failure(let databaseError): + continuation.resume(throwing: databaseError) } } } } - - func selectPendingCount(_ completion: @escaping DatabaseIntCompletionBlock) { - queue.runInDatabase { databaseResult in - var count: Int = 0 - var error: DatabaseError? - func makeDatabaseCall(_ database: FMDatabase) { - let sql = "select count(*) from syncStatus" - if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) { - count = self.numberWithCountResultSet(resultSet) + func selectPendingCount() async throws -> Int { + + try await withCheckedThrowingContinuation { continuation in + queue.runInDatabase { databaseResult in + + func makeDatabaseCall(_ database: FMDatabase) -> Int { + let sql = "select count(*) from syncStatus" + var count = 0 + if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) { + count = self.numberWithCountResultSet(resultSet) + } + return count } - } - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - case .failure(let databaseError): - error = databaseError - } - - DispatchQueue.main.async { - if let error = error { - completion(.failure(error)) - } - else { - completion(.success(count)) + switch databaseResult { + case .success(let database): + let count = makeDatabaseCall(database) + continuation.resume(returning: count) + case .failure(let databaseError): + continuation.resume(throwing: databaseError) } } } @@ -91,104 +82,107 @@ struct SyncStatusTable: DatabaseTable { return try await selectPendingArticleIDs(.read) } - func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - selectPendingArticleIDsAsync(.read, completion) - } - func selectPendingStarredArticleIDs() async throws -> Set { return try await selectPendingArticleIDs(.starred) } - func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { - selectPendingArticleIDsAsync(.starred, completion) - } - - func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { - queue.runInTransaction { databaseResult in + func resetAllSelectedForProcessing() async throws { - func makeDatabaseCall(_ database: FMDatabase) { - let updateSQL = "update syncStatus set selected = false" - database.executeUpdate(updateSQL, withArgumentsIn: nil) - } + try await withCheckedThrowingContinuation { continuation in + queue.runInTransaction { databaseResult in - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - callCompletion(completion, nil) - case .failure(let databaseError): - callCompletion(completion, databaseError) + func makeDatabaseCall(_ database: FMDatabase) { + let updateSQL = "update syncStatus set selected = false" + database.executeUpdate(updateSQL, withArgumentsIn: nil) + } + + switch databaseResult { + case .success(let database): + makeDatabaseCall(database) + continuation.resume() + case .failure(let databaseError): + continuation.resume(throwing: databaseError) + } } } } - func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { - guard !articleIDs.isEmpty else { - callCompletion(completion, nil) - return - } - - queue.runInTransaction { databaseResult in + func resetSelectedForProcessing(_ articleIDs: [String]) async throws { - func makeDatabaseCall(_ database: FMDatabase) { - let parameters = articleIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! - let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" - database.executeUpdate(updateSQL, withArgumentsIn: parameters) + try await withCheckedThrowingContinuation { continuation in + guard !articleIDs.isEmpty else { + continuation.resume() + return } - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - callCompletion(completion, nil) - case .failure(let databaseError): - callCompletion(completion, databaseError) + queue.runInTransaction { databaseResult in + + func makeDatabaseCall(_ database: FMDatabase) { + let parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" + database.executeUpdate(updateSQL, withArgumentsIn: parameters) + } + + switch databaseResult { + case .success(let database): + makeDatabaseCall(database) + continuation.resume() + case .failure(let databaseError): + continuation.resume(throwing: databaseError) + } } } } - - func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { - guard !articleIDs.isEmpty else { - callCompletion(completion, nil) - return - } - - queue.runInTransaction { databaseResult in - func makeDatabaseCall(_ database: FMDatabase) { - let parameters = articleIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! - let deleteSQL = "delete from syncStatus where selected = true and articleID in \(placeholders)" - database.executeUpdate(deleteSQL, withArgumentsIn: parameters) + func deleteSelectedForProcessing(_ articleIDs: [String]) async throws { + + try await withCheckedThrowingContinuation { continuation in + guard !articleIDs.isEmpty else { + continuation.resume() + return } - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - callCompletion(completion, nil) - case .failure(let databaseError): - callCompletion(completion, databaseError) + queue.runInTransaction { databaseResult in + + func makeDatabaseCall(_ database: FMDatabase) { + let parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + let deleteSQL = "delete from syncStatus where selected = true and articleID in \(placeholders)" + database.executeUpdate(deleteSQL, withArgumentsIn: parameters) + } + + switch databaseResult { + case .success(let database): + makeDatabaseCall(database) + continuation.resume() + case .failure(let databaseError): + continuation.resume(throwing: databaseError) + } } } } - - func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { - queue.runInTransaction { databaseResult in - func makeDatabaseCall(_ database: FMDatabase) { - let statusArray = statuses.map { $0.databaseDictionary() } - self.insertRows(statusArray, insertType: .orReplace, in: database) - } + func insertStatuses(_ statuses: [SyncStatus]) async throws { - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - callCompletion(completion, nil) - case .failure(let databaseError): - callCompletion(completion, databaseError) + try await withCheckedThrowingContinuation { continuation in + queue.runInTransaction { databaseResult in + + func makeDatabaseCall(_ database: FMDatabase) { + let statusArray = statuses.map { $0.databaseDictionary() } + self.insertRows(statusArray, insertType: .orReplace, in: database) + } + + switch databaseResult { + case .success(let database): + makeDatabaseCall(database) + continuation.resume() + case .failure(let databaseError): + continuation.resume(throwing: databaseError) + } } } } - } private extension SyncStatusTable { @@ -206,59 +200,30 @@ private extension SyncStatusTable { return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected) } - func selectPendingArticleIDs(_ statusKey: ArticleStatus.Key) async throws -> Set { - return try await withCheckedThrowingContinuation() { continuation in - Task { @MainActor in - selectPendingArticleIDsAsync(statusKey) { result in - switch result { - case .success(let articleIDs): - continuation.resume(returning: articleIDs) - case .failure(let databaseError): - continuation.resume(throwing: databaseError) - } - } - } - } - } + func selectPendingArticleIDs(_ statusKey: ArticleStatus.Key) async throws -> Set { - func selectPendingArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ completion: @escaping SyncStatusArticleIDsCompletionBlock) { + return try await withCheckedThrowingContinuation { continuation in + queue.runInDatabase { databaseResult in - queue.runInDatabase { databaseResult in + func makeDatabaseCall(_ database: FMDatabase) -> Set { + let sql = "select articleID from syncStatus where selected == false and key = \"\(statusKey.rawValue)\";" - func makeDatabaseCall(_ database: FMDatabase) { - let sql = "select articleID from syncStatus where selected == false and key = \"\(statusKey.rawValue)\";" + guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { + return Set() + } - guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { - DispatchQueue.main.async { - completion(.success(Set())) - } - return - } + let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } + return articleIDs + } - let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } - DispatchQueue.main.async { - completion(.success(articleIDs)) - } - } - - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - case .failure(let databaseError): - DispatchQueue.main.async { - completion(.failure(databaseError)) - } - } - } - } - -} - -private func callCompletion(_ completion: DatabaseCompletionBlock?, _ databaseError: DatabaseError?) { - guard let completion = completion else { - return - } - DispatchQueue.main.async { - completion(databaseError) + switch databaseResult { + case .success(let database): + let articleIDs = makeDatabaseCall(database) + continuation.resume(returning: articleIDs) + case .failure(let databaseError): + continuation.resume(throwing: databaseError) + } + } + } } }