From c7b036f3644aae085a98efdd30eec18f3441450d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 17 Nov 2023 22:23:33 -0800 Subject: [PATCH] Use RSCore 3. --- Account/Package.swift | 2 +- Account/Sources/Account/Account.swift | 11 +- Account/Sources/Account/AccountDelegate.swift | 4 +- .../FeedbinAccountDelegate.swift | 33 ++- .../LocalAccountDelegate.swift | 10 +- .../NewsBlurAccountDelegate+Internal.swift | 9 +- .../NewsBlurAccountDelegate.swift | 32 ++- .../ReaderAPIAccountDelegate.swift | 34 ++- Account/Sources/Account/AccountManager.swift | 7 +- .../CloudKit/CloudKitAccountDelegate.swift | 270 +++++++++++------- .../CloudKit/CloudKitAccountZone.swift | 38 +++ .../CloudKitAccountZoneDelegate.swift | 61 ++-- .../CloudKit/CloudKitArticlesZone.swift | 25 +- .../CloudKitArticlesZoneDelegate.swift | 19 +- .../CloudKitReceiveStatusOperation.swift | 13 +- .../CloudKitRemoteNotificationOperation.swift | 12 +- .../Feedly/FeedlyAccountDelegate.swift | 52 ++-- Articles/Package.swift | 2 +- ArticlesDatabase/Package.swift | 2 +- FeedFinder/Package.swift | 2 +- NetNewsWire.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- SyncClients/Feedbin/Package.swift | 2 +- SyncClients/LocalAccount/Package.swift | 2 +- SyncClients/NewsBlur/Package.swift | 2 +- SyncClients/ReaderAPI/Package.swift | 2 +- SyncDatabase/Package.swift | 2 +- 27 files changed, 446 insertions(+), 208 deletions(-) diff --git a/Account/Package.swift b/Account/Package.swift index 4e4aa9649..6cb758680 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 891c57c9f..c6c957c66 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -432,15 +432,8 @@ public enum FetchType { try await delegate.refreshAll(for: self) } - public func sendArticleStatus(completion: ((Result) -> Void)? = nil) { - delegate.sendArticleStatus(for: self) { result in - switch result { - case .success: - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) - } - } + public func sendArticleStatus() async throws { + try await delegate.sendArticleStatus(for: self) } public func syncArticleStatus() async throws { diff --git a/Account/Sources/Account/AccountDelegate.swift b/Account/Sources/Account/AccountDelegate.swift index 403df0d80..e1097b460 100644 --- a/Account/Sources/Account/AccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegate.swift @@ -27,8 +27,8 @@ import Secrets func refreshAll(for account: Account) async throws func syncArticleStatus(for account: Account) async throws - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) + func sendArticleStatus(for account: Account) async throws + func refreshArticleStatus(for account: Account) async throws func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) diff --git a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index b67f10729..42540234b 100644 --- a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -25,7 +25,7 @@ public enum FeedbinAccountDelegateError: String, Error { @MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging { private let database: SyncDatabase - + private let caller: FeedbinAPICaller let behaviors: AccountBehaviors = [.disallowFeedCopyInRootFolder] @@ -128,7 +128,20 @@ public enum FeedbinAccountDelegateError: String, Error { } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + 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) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { Task { @MainActor in logger.debug("Sending article statuses") @@ -190,7 +203,21 @@ public enum FeedbinAccountDelegateError: String, Error { } } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + 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) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Refreshing article statuses...") diff --git a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift index 06eda029a..0e1553c86 100644 --- a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift @@ -76,14 +76,14 @@ final class LocalAccountDelegate: AccountDelegate, Logging { return } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + func sendArticleStatus(for account: Account) async throws { + return } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - completion(.success(())) + func refreshArticleStatus(for account: Account) async throws { + return } - + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { var fileData: Data? diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift index b93d06dfb..610dca162 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift @@ -469,9 +469,9 @@ extension NewsBlurAccountDelegate { // Download the initial articles downloadFeed(account: account, feed: feed, page: 1) { result in - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await self.refreshArticleStatus(for: account) self.refreshMissingStories(for: account) { result in switch result { case .success: @@ -485,8 +485,7 @@ extension NewsBlurAccountDelegate { completion(.failure(error)) } } - - case .failure(let error): + } catch { completion(.failure(error)) } } diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift index e1ea7eb7e..bcada0350 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift @@ -151,7 +151,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + 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) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { Task { @MainActor in logger.debug("Sending story statuses") @@ -223,7 +237,21 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging { } } - func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + 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) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { logger.debug("Refreshing story statuses...") let group = DispatchGroup() diff --git a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift index 2bfc7d76a..e1993d908 100644 --- a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift @@ -39,7 +39,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { @MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging { private let variant: ReaderAPIVariant - + private let database: SyncDatabase private let caller: ReaderAPICaller @@ -206,7 +206,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + 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) + } + } + } + } + + private func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Sending article statuses") @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) { @@ -253,7 +267,21 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } } - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + 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) + } + } + } + } + + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { logger.debug("Refreshing article statuses...") let group = DispatchGroup() diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 835989fb4..92512c4d5 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -267,10 +267,12 @@ import RSDatabase public func sendArticleStatusAll(completion: (() -> Void)? = nil) { let group = DispatchGroup() - + for account in activeAccounts { group.enter() - account.sendArticleStatus() { _ in + + Task { @MainActor in + try? await account.sendArticleStatus() group.leave() } } @@ -280,6 +282,7 @@ import RSDatabase } } + public func syncArticleStatusAll() async { await withTaskGroup(of: Void.self) { group in diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 423f86e52..3566058e9 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -102,42 +102,30 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } func syncArticleStatus(for account: Account) async throws { + try await sendArticleStatus(for: account) + } + + func sendArticleStatus(for account: Account) async throws { + try await sendArticleStatus(for: account, showProgress: false) + } + + func refreshArticleStatus(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) + + let op = CloudKitReceiveStatusOperation(articlesZone: self.articlesZone) + op.completionBlock = { mainThreadOperaion in + if mainThreadOperaion.isCanceled { + continuation.resume(throwing: CloudKitAccountDelegateError.unknown) + } else { + continuation.resume() } } + + mainThreadOperationQueue.add(op) } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - sendArticleStatus(for: account, showProgress: false, completion: completion) - } - - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone) - op.completionBlock = { mainThreadOperaion in - if mainThreadOperaion.isCanceled { - completion(.failure(CloudKitAccountDelegateError.unknown)) - } else { - completion(.success(())) - } - } - mainThreadOperationQueue.add(op) - } - func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { guard refreshProgress.isComplete else { completion(.success(())) @@ -454,7 +442,7 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { try? await self.database.insertStatuses(syncStatuses) if let count = try? await self.database.selectPendingCount(), count > 100 { - self.sendArticleStatus(for: account, showProgress: false) { _ in } + try? await self.sendArticleStatus(for: account, showProgress: false) } completion(.success(())) case .failure(let error): @@ -517,52 +505,92 @@ final class CloudKitAccountDelegate: AccountDelegate, Logging { } private extension CloudKitAccountDelegate { - - func initialRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { - - func fail(_ error: Error) { - self.processAccountError(account, error) - self.refreshProgress.clear() - completion(.failure(error)) - } - - refreshProgress.isIndeterminate = true - refreshProgress.addToNumberOfTasksAndRemaining(3) - accountZone.fetchChangesInZone() { result in - self.refreshProgress.completeTask() - let feeds = account.flattenedFeeds() - self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) + func fetchChangesInZone() async throws { - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - self.refreshProgress.completeTask() - self.refreshProgress.isIndeterminate = false - switch result { - case .success: - - self.combinedRefresh(account, feeds) { result in - self.refreshProgress.clear() - switch result { - case .success: - account.metadata.lastArticleFetchEndTime = Date() - case .failure(let error): - fail(error) - } - } - - case .failure(let error): - fail(error) - } + try await withCheckedThrowingContinuation { continuation in + self.accountZone.fetchChangesInZone { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - fail(error) } } - } + @MainActor func initialRefreshAll(for account : Account) async throws { + + refreshProgress.isIndeterminate = true + refreshProgress.addToNumberOfTasksAndRemaining(3) + + do { + try await fetchChangesInZone() + refreshProgress.completeTask() + + let feeds = account.flattenedFeeds() + refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) + + try await refreshArticleStatus(for: account) + refreshProgress.completeTask() + refreshProgress.isIndeterminate = false + + await combinedRefresh(account, feeds) + refreshProgress.clear() + account.metadata.lastArticleFetchEndTime = Date() + } catch { + processAccountError(account, error) + refreshProgress.clear() + throw error + } + } + +// func initialRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { +// +// func fail(_ error: Error) { +// self.processAccountError(account, error) +// self.refreshProgress.clear() +// completion(.failure(error)) +// } +// +// refreshProgress.isIndeterminate = true +// refreshProgress.addToNumberOfTasksAndRemaining(3) +// accountZone.fetchChangesInZone() { result in +// self.refreshProgress.completeTask() +// +// let feeds = account.flattenedFeeds() +// self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count) +// +// switch result { +// case .success: +// self.refreshArticleStatus(for: account) { result in +// self.refreshProgress.completeTask() +// self.refreshProgress.isIndeterminate = false +// switch result { +// case .success: +// +// self.combinedRefresh(account, feeds) { result in +// self.refreshProgress.clear() +// switch result { +// case .success: +// account.metadata.lastArticleFetchEndTime = Date() +// case .failure(let error): +// fail(error) +// } +// } +// +// case .failure(let error): +// fail(error) +// } +// } +// case .failure(let error): +// fail(error) +// } +// } +// +// } + func standardRefreshAll(for account: Account, completion: @escaping (Result) -> Void) { let intialFeedsCount = account.flattenedFeeds().count @@ -583,6 +611,36 @@ private extension CloudKitAccountDelegate { let feeds = account.flattenedFeeds() self.refreshProgress.addToNumberOfTasksAndRemaining(feeds.count - intialFeedsCount) + Task { @MainActor in + do { + try await self.refreshArticleStatus(for: account) + self.refreshProgress.completeTask() + self.refreshProgress.isIndeterminate = false + + self.combinedRefresh(account, feeds) { result in + do { + try await self.sendArticleStatus(for: account, showProgress: true) + if case .failure(let error) = result { + fail(error) + } else { + account.metadata.lastArticleFetchEndTime = Date() + completion(.success(())) + } + } catch { + fail(error) + } + } + + } catch { + self.refreshProgress.completeTask() + self.refreshProgress.isIndeterminate = false + } + + + + + } + self.refreshArticleStatus(for: account) { result in switch result { case .success: @@ -611,20 +669,31 @@ private extension CloudKitAccountDelegate { } - func combinedRefresh(_ account: Account, _ feeds: Set, completion: @escaping (Result) -> Void) { + func combinedRefresh(_ account: Account, _ feeds: Set) async { let feedURLs = Set(feeds.map{ $0.url }) - let group = DispatchGroup() + + try await withCheckedContinuation { continuation in + refresher.refreshFeedURLs(feedURLs) { + continuation.resume() + } + } + } - group.enter() - refresher.refreshFeedURLs(feedURLs) { - group.leave() - } - - group.notify(queue: DispatchQueue.main) { - completion(.success(())) - } - } +// func combinedRefresh(_ account: Account, _ feeds: Set, completion: @escaping (Result) -> Void) { +// +// let feedURLs = Set(feeds.map{ $0.url }) +// let group = DispatchGroup() +// +// group.enter() +// refresher.refreshFeedURLs(feedURLs) { +// group.leave() +// } +// +// group.notify(queue: DispatchQueue.main) { +// completion(.success(())) +// } +// } func createRSSFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { @@ -739,12 +808,13 @@ private extension CloudKitAccountDelegate { case .success(let articles): self.storeArticleChanges(new: articles, updated: Set
(), deleted: Set
()) { self.refreshProgress.completeTask() - self.sendArticleStatus(for: account, showProgress: true) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { _ in } - case .failure(let error): - self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)") + + Task { @MainActor in + do { + try await self.sendArticleStatus(for: account, showProgress: true) + try await self.refreshArticleStatus(for: account) + } catch { + self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)") } } } @@ -805,20 +875,24 @@ private extension CloudKitAccountDelegate { } } - func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result) -> Void)) { - let op = CloudKitSendStatusOperation(account: account, - articlesZone: articlesZone, - refreshProgress: refreshProgress, - showProgress: showProgress, - database: database) - op.completionBlock = { mainThreadOperaion in - if mainThreadOperaion.isCanceled { - completion(.failure(CloudKitAccountDelegateError.unknown)) - } else { - completion(.success(())) + func sendArticleStatus(for account: Account, showProgress: Bool) async throws { + + try await withCheckedThrowingContinuation { continuation in + let op = CloudKitSendStatusOperation(account: account, + articlesZone: self.articlesZone, + refreshProgress: self.refreshProgress, + showProgress: showProgress, + database: self.database) + op.completionBlock = { mainThreadOperation in + if mainThreadOperation.isCanceled { + continuation.resume(throwing: CloudKitAccountDelegateError.unknown) + } else { + continuation.resume() + } } + + mainThreadOperationQueue.add(op) } - mainThreadOperationQueue.add(op) } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index 812fb90f8..bbb3e0e15 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -253,6 +253,29 @@ final class CloudKitAccountZone: CloudKitZone { } } + func findOrCreateAccount() async throws -> String { + + guard let database else { + return + } + + let predicate = NSPredicate(format: "isAccount = \"1\"") + let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) + + do { + let records = try await database.perform(ckQuery, inZoneWith: zoneID) + if records.count > 0 { + return records[0].externalID + } else { + createContainer(name: "Account", isAccount: true, completion: completion) + } + } catch { + switch CloudKitZoneResult.resolve(error) { + } + } + + } + func findOrCreateAccount(completion: @escaping (Result) -> Void) { let predicate = NSPredicate(format: "isAccount = \"1\"") let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate) @@ -343,6 +366,21 @@ private extension CloudKitAccountZone { return record } + func createContainer(name: String, isAccount: Bool) async throws -> String { + let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID()) + record[CloudKitContainer.Fields.name] = name + record[CloudKitContainer.Fields.isAccount] = isAccount ? "1" : "0" + + save(record) { result in + switch result { + case .success: + completion(.success(record.externalID)) + case .failure(let error): + completion(.failure(error)) + } + } + } + func createContainer(name: String, isAccount: Bool, completion: @escaping (Result) -> Void) { let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID()) record[CloudKitContainer.Fields.name] = name diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index bd676c6d9..6ccd89865 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -31,32 +31,19 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { self.articlesZone = articlesZone } - @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { - for deletedRecordKey in deleted { - switch deletedRecordKey.recordType { - case CloudKitAccountZone.CloudKitFeed.recordType: - removeFeed(deletedRecordKey.recordID.externalID) - case CloudKitAccountZone.CloudKitContainer.recordType: - removeContainer(deletedRecordKey.recordID.externalID) - default: - assertionFailure("Unknown record type: \(deletedRecordKey.recordType)") + @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws { + try await withCheckedThrowingContinuation { continuation in + self.cloudKitWasChanged(updated: updated, deleted: deleted) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } } } - - for changedRecord in updated { - switch changedRecord.recordType { - case CloudKitAccountZone.CloudKitFeed.recordType: - addOrUpdateFeed(changedRecord) - case CloudKitAccountZone.CloudKitContainer.recordType: - addOrUpdateContainer(changedRecord) - default: - assertionFailure("Unknown record type: \(changedRecord.recordType)") - } - } - - completion(.success(())) } - + @MainActor func addOrUpdateFeed(_ record: CKRecord) { guard let account = account, let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String, @@ -140,7 +127,33 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { private extension CloudKitAcountZoneDelegate { - @MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { + @MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { + for deletedRecordKey in deleted { + switch deletedRecordKey.recordType { + case CloudKitAccountZone.CloudKitFeed.recordType: + removeFeed(deletedRecordKey.recordID.externalID) + case CloudKitAccountZone.CloudKitContainer.recordType: + removeContainer(deletedRecordKey.recordID.externalID) + default: + assertionFailure("Unknown record type: \(deletedRecordKey.recordType)") + } + } + + for changedRecord in updated { + switch changedRecord.recordType { + case CloudKitAccountZone.CloudKitFeed.recordType: + addOrUpdateFeed(changedRecord) + case CloudKitAccountZone.CloudKitContainer.recordType: + addOrUpdateContainer(changedRecord) + default: + assertionFailure("Unknown record type: \(changedRecord.recordType)") + } + } + + completion(.success(())) + } + + @MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) { guard let account = account else { return } feed.name = name diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index a565ab9c8..590b25d37 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -84,7 +84,30 @@ final class CloudKitArticlesZone: CloudKitZone { self.save(compressedRecords, completion: completion) } } - + + @MainActor func saveNewArticles(_ articles: Set
) async throws { + guard !articles.isEmpty else { + return + } + + let records: [CKRecord] = { + var recordsAccumulator = [CKRecord]() + + let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true } + for saveArticle in saveArticles { + recordsAccumulator.append(makeStatusRecord(saveArticle)) + recordsAccumulator.append(makeArticleRecord(saveArticle)) + } + return recordsAccumulator + }() + + compressionQueue.async { + let compressedRecords = self.compressArticleRecords(records) + self.save(compressedRecords, completion: completion) + } + } + + func deleteArticles(_ feedExternalID: String, completion: @escaping ((Result) -> Void)) { let predicate = NSPredicate(format: "webFeedExternalID = %@", feedExternalID) let ckQuery = CKQuery(recordType: CloudKitArticleStatus.recordType, predicate: predicate) diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index a86568a25..4af872619 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -28,6 +28,22 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { self.articlesZone = articlesZone } + func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey]) async throws { + try await withCheckedThrowingContinuation { continuation in + cloudKitWasChanged(updated: updated, deleted: deleted) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} + +private extension CloudKitArticlesZoneDelegate { + func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void) { Task { @MainActor in @@ -54,9 +70,6 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging { } } } -} - -private extension CloudKitArticlesZoneDelegate { func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set, completion: @escaping (Error?) -> Void) { let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID }) diff --git a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift index 91d0f0789..f58963c00 100644 --- a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift @@ -32,16 +32,15 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging { logger.debug("Refreshing article statuses...") - articlesZone.fetchChangesInZone() { result in - self.logger.debug("Done refreshing article statuses.") - switch result { - case .success: + Task { @MainActor in + do { + try await articlesZone.fetchChangesInZone() + self.logger.debug("Done refreshing article statuses.") self.operationDelegate?.operationDidComplete(self) - case .failure(let error): - self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)") + } catch { + self.logger.error("Receive status error: \(error.localizedDescription, privacy: .public)") self.operationDelegate?.cancelOperation(self) } } } - } diff --git a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift index 73833af34..62ff6c5f0 100644 --- a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift @@ -36,13 +36,11 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation, Logging { logger.debug("Processing remote notification...") - accountZone.receiveRemoteNotification(userInfo: userInfo) { - articlesZone.receiveRemoteNotification(userInfo: self.userInfo) { - self.logger.debug("Done processing remote notification.") - self.operationDelegate?.operationDidComplete(self) - } + Task { @MainActor in + await accountZone.receiveRemoteNotification(userInfo: self.userInfo) + await articlesZone.receiveRemoteNotification(userInfo: self.userInfo) + self.logger.debug("Done processing remote notification.") + self.operationDelegate?.operationDidComplete(self) } - } - } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 701b3a1e2..188af7369 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -148,46 +148,48 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { func syncArticleStatus(for account: Account) async throws { + do { + try await sendArticleStatus(for: account) + try await refreshArticleStatus(for: account) + } catch { + self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") + throw error + } + } + + func sendArticleStatus(for account: Account) async throws { + // Ensure remote articles have the same status as they do locally. + try await withCheckedThrowingContinuation { continuation in - sendArticleStatus(for: account) { result in + let send = FeedlySendArticleStatusesOperation(database: database, service: caller) + send.completionBlock = { operation in + continuation.resume() + } + + 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: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - self.logger.error("Failed to refresh article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") - continuation.resume(throwing: error) - } - } + continuation.resume() case .failure(let error): - self.logger.error("Failed to send article status for account \(String(describing: account.type), privacy: .public): \(error.localizedDescription, privacy: .public)") continuation.resume(throwing: error) } } } } - func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - // Ensure remote articles have the same status as they do locally. - let send = FeedlySendArticleStatusesOperation(database: database, service: caller) - send.completionBlock = { operation in - // TODO: not call with success if operation was canceled? Not sure. - DispatchQueue.main.async { - completion(.success(())) - } - } - operationQueue.add(send) - } - /// Attempts to ensure local articles have the same status as they do remotely. /// So if the user is using another client roughly simultaneously with this app, /// 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. - func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { + private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { guard let credentials = credentials else { return completion(.success(())) } @@ -557,7 +559,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging { try await self.database.insertStatuses(syncStatuses) let count = try await self.database.selectPendingCount() if count > 100 { - self.sendArticleStatus(for: account) { _ in } + try? await self.sendArticleStatus(for: account) } completion(.success(())) } catch { diff --git a/Articles/Package.swift b/Articles/Package.swift index 2de47b65a..e76db645c 100644 --- a/Articles/Package.swift +++ b/Articles/Package.swift @@ -11,7 +11,7 @@ let package = Package( targets: ["Articles"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), ], targets: [ .target( diff --git a/ArticlesDatabase/Package.swift b/ArticlesDatabase/Package.swift index 321bdf053..1eca4bf7f 100644 --- a/ArticlesDatabase/Package.swift +++ b/ArticlesDatabase/Package.swift @@ -4,7 +4,7 @@ import PackageDescription var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), ] diff --git a/FeedFinder/Package.swift b/FeedFinder/Package.swift index ffc034913..ede1458cd 100644 --- a/FeedFinder/Package.swift +++ b/FeedFinder/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["FeedFinder"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(path: "../AccountError"), diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index f7441f472..10c67560e 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -5272,7 +5272,7 @@ repositoryURL = "https://github.com/Ranchero-Software/RSCore.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.1; + minimumVersion = 3.0.0; }; }; 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = { diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fa8ca3b9..9238f1837 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Ranchero-Software/RSCore.git", "state" : { - "revision" : "dcaa40ceb2c8acd182fbcd69d1f8e56df97e38b1", - "version" : "2.0.1" + "revision" : "cee6d96e036cc4ad08ad5f79364f87f9c291c43c", + "version" : "3.0.0" } }, { diff --git a/SyncClients/Feedbin/Package.swift b/SyncClients/Feedbin/Package.swift index 78fb37d03..74a2ec88b 100644 --- a/SyncClients/Feedbin/Package.swift +++ b/SyncClients/Feedbin/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../../Secrets"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncClients/LocalAccount/Package.swift b/SyncClients/LocalAccount/Package.swift index be98a5944..2b80c9a6a 100644 --- a/SyncClients/LocalAccount/Package.swift +++ b/SyncClients/LocalAccount/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["LocalAccount"]), ], dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), ], diff --git a/SyncClients/NewsBlur/Package.swift b/SyncClients/NewsBlur/Package.swift index 6b151adbe..c31427dcd 100644 --- a/SyncClients/NewsBlur/Package.swift +++ b/SyncClients/NewsBlur/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../../Secrets"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncClients/ReaderAPI/Package.swift b/SyncClients/ReaderAPI/Package.swift index 32102ad88..045a21569 100644 --- a/SyncClients/ReaderAPI/Package.swift +++ b/SyncClients/ReaderAPI/Package.swift @@ -16,7 +16,7 @@ let package = Package( .package(path: "../../Secrets"), .package(path: "../../AccountError"), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) ], targets: [ diff --git a/SyncDatabase/Package.swift b/SyncDatabase/Package.swift index ea12311b5..49f6e2dcd 100644 --- a/SyncDatabase/Package.swift +++ b/SyncDatabase/Package.swift @@ -2,7 +2,7 @@ import PackageDescription var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "3.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), ]