diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 24d20e47e..5c01172ae 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -42,7 +42,7 @@ final class CloudKitAccountDelegate: AccountDelegate { private let accountZone: CloudKitAccountZone private let articlesZone: CloudKitArticlesZone - private let mainThreadOperationQueue = MainThreadOperationQueue() + @MainActor private let mainThreadOperationQueue = MainThreadOperationQueue() private lazy var refresher: LocalAccountRefresher = { let refresher = LocalAccountRefresher() diff --git a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift index d73c35564..348a8bd08 100644 --- a/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitReceiveStatusOperation.swift @@ -27,7 +27,7 @@ class CloudKitReceiveStatusOperation: MainThreadOperation { self.articlesZone = articlesZone } - func run() { + @MainActor func run() { guard let articlesZone = articlesZone else { self.operationDelegate?.operationDidComplete(self) return diff --git a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift index 7ae382ef1..fc5458e01 100644 --- a/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitRemoteNotificationOperation.swift @@ -31,7 +31,7 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation { self.userInfo = userInfo } - func run() { + @MainActor func run() { guard let accountZone = accountZone, let articlesZone = articlesZone else { self.operationDelegate?.operationDidComplete(self) return diff --git a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift index 38a7e2811..4737f559b 100644 --- a/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift +++ b/Account/Sources/Account/CloudKit/CloudKitSendStatusOperation.swift @@ -41,20 +41,22 @@ class CloudKitSendStatusOperation: MainThreadOperation { self.database = database } - func run() { + @MainActor func run() { os_log(.debug, log: log, "Sending article statuses...") if showProgress { database.selectPendingCount() { result in - switch result { - case .success(let count): - let ticks = count / self.blockSize - self.refreshProgress?.addToNumberOfTasksAndRemaining(ticks) - self.selectForProcessing() - case .failure(let databaseError): - os_log(.error, log: self.log, "Send status count pending error: %@.", databaseError.localizedDescription) - self.operationDelegate?.cancelOperation(self) + MainActor.assumeIsolated { + switch result { + case .success(let count): + let ticks = count / self.blockSize + self.refreshProgress?.addToNumberOfTasksAndRemaining(ticks) + self.selectForProcessing() + case .failure(let databaseError): + os_log(.error, log: self.log, "Send status count pending error: %@.", databaseError.localizedDescription) + self.operationDelegate?.cancelOperation(self) + } } } @@ -72,33 +74,36 @@ private extension CloudKitSendStatusOperation { func selectForProcessing() { database.selectForProcessing(limit: blockSize) { result in - switch result { - case .success(let syncStatuses): - - func stopProcessing() { - if self.showProgress { - self.refreshProgress?.completeTask() + + MainActor.assumeIsolated { + switch result { + case .success(let syncStatuses): + + @MainActor func stopProcessing() { + if self.showProgress { + self.refreshProgress?.completeTask() + } + os_log(.debug, log: self.log, "Done sending article statuses.") + self.operationDelegate?.operationDidComplete(self) } - os_log(.debug, log: self.log, "Done sending article statuses.") - self.operationDelegate?.operationDidComplete(self) - } - - guard syncStatuses.count > 0 else { - stopProcessing() - return - } - - self.processStatuses(syncStatuses) { stop in - if stop { + + guard syncStatuses.count > 0 else { stopProcessing() - } else { - self.selectForProcessing() + return } + + self.processStatuses(syncStatuses) { stop in + if stop { + stopProcessing() + } else { + self.selectForProcessing() + } + } + + case .failure(let databaseError): + os_log(.error, log: self.log, "Send status error: %@.", databaseError.localizedDescription) + self.operationDelegate?.cancelOperation(self) } - - case .failure(let databaseError): - os_log(.error, log: self.log, "Send status error: %@.", databaseError.localizedDescription) - self.operationDelegate?.cancelOperation(self) } } } diff --git a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index 224428b2b..92e73fe30 100644 --- a/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -65,8 +65,8 @@ final class FeedlyAccountDelegate: AccountDelegate { private let database: SyncDatabase private weak var currentSyncAllOperation: MainThreadOperation? - private let operationQueue = MainThreadOperationQueue() - + @MainActor private let operationQueue = MainThreadOperationQueue() + init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API, secretsProvider: SecretsProvider) { // Many operations have their own operation queues, such as the sync all operation. // Making this a serial queue at this higher level of abstraction means we can ensure, @@ -550,8 +550,10 @@ final class FeedlyAccountDelegate: AccountDelegate { /// Suspend all network activity func suspendNetwork() { - caller.suspend() - operationQueue.cancelAllOperations() + MainActor.assumeIsolated { + caller.suspend() + operationQueue.cancelAllOperations() + } } /// Suspend the SQLLite databases @@ -572,7 +574,7 @@ final class FeedlyAccountDelegate: AccountDelegate { extension FeedlyAccountDelegate: FeedlyAPICallerDelegate { - func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) { + @MainActor func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) { guard let account = initializedAccount else { completionHandler(false) return @@ -600,8 +602,6 @@ extension FeedlyAccountDelegate: FeedlyAPICallerDelegate { completionHandler(refreshAccessTokenDelegate.didReauthorize && !operation.isCanceled) } - Task { @MainActor in - MainThreadOperationQueue.shared.add(refreshAccessToken) - } + MainThreadOperationQueue.shared.add(refreshAccessToken) } } diff --git a/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift index 8b3264e65..21ada7d3c 100644 --- a/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift +++ b/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -51,7 +51,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { self.oauthClient = Account.oauthAuthorizationClient(for: accountType, secretsProvider: secretsProvider) } - public func run() { + @MainActor public func run() { assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, secretsProvider: secretsProvider) @@ -101,28 +101,30 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { } private func didEndAuthentication(url: URL?, error: Error?) { - guard !isCanceled else { - didFinish() - return - } - - do { - guard let url = url else { - if let error = error { - throw error - } - throw URLError(.badURL) + MainActor.assumeIsolated { + guard !isCanceled else { + didFinish() + return + } + + do { + guard let url = url else { + if let error = error { + throw error + } + throw URLError(.badURL) + } + + let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) + + Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, secretsProvider: secretsProvider, completion: didEndRequestingAccessToken(_:)) + + } catch is ASWebAuthenticationSessionError { + didFinish() // Primarily, cancellation. + + } catch { + didFinish(error) } - - let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) - - Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, secretsProvider: secretsProvider, completion: didEndRequestingAccessToken(_:)) - - } catch is ASWebAuthenticationSessionError { - didFinish() // Primarily, cancellation. - - } catch { - didFinish(error) } } @@ -174,12 +176,12 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { // MARK: Managing Operation State - private func didFinish() { + @MainActor private func didFinish() { assert(Thread.isMainThread) operationDelegate?.operationDidComplete(self) } - private func didFinish(_ error: Error) { + @MainActor private func didFinish(_ error: Error) { assert(Thread.isMainThread) delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error) didFinish() diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift index a36feb27b..26d524656 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift @@ -12,7 +12,7 @@ import RSWeb import Secrets import Core -class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { +@MainActor final class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { private let operationQueue = MainThreadOperationQueue() var addCompletionHandler: ((Result) -> ())? diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift index bc37dfdb7..1684561cd 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift @@ -23,13 +23,15 @@ final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEn override func run() { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in - switch result { - case .success(let articleIds): - self.entryIds.formUnion(articleIds) - self.didFinish() - - case .failure(let error): - self.didFinish(with: error) + MainActor.assumeIsolated { + switch result { + case .success(let articleIds): + self.entryIds.formUnion(articleIds) + self.didFinish() + + case .failure(let error): + self.didFinish(with: error) + } } } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift index 3b6d940b5..646e06db2 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -78,14 +78,16 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { } database.selectPendingStarredStatusArticleIDs { result in - switch result { - case .success(let pendingArticleIds): - self.remoteEntryIds.subtract(pendingArticleIds) - - self.updateStarredStatuses() - - case .failure(let error): - self.didFinish(with: error) + MainActor.assumeIsolated { + switch result { + case .success(let pendingArticleIds): + self.remoteEntryIds.subtract(pendingArticleIds) + + self.updateStarredStatuses() + + case .failure(let error): + self.didFinish(with: error) + } } } } @@ -97,12 +99,14 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { } account.fetchStarredArticleIDs { result in - switch result { - case .success(let localStarredArticleIDs): - self.processStarredArticleIDs(localStarredArticleIDs) - - case .failure(let error): - self.didFinish(with: error) + MainActor.assumeIsolated { + switch result { + case .success(let localStarredArticleIDs): + self.processStarredArticleIDs(localStarredArticleIDs) + + case .failure(let error): + self.didFinish(with: error) + } } } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift index 0e077f8c7..d35a22920 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift @@ -52,19 +52,22 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation { switch result { case .success(let streamIds): account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in - - if let error = databaseError { - self.didFinish(with: error) - return + + MainActor.assumeIsolated { + if let error = databaseError { + self.didFinish(with: error) + return + } + + 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) } - - 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) + } case .failure(let error): didFinish(with: error) diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift index 38d970ee7..882da8b85 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -79,14 +79,16 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { } database.selectPendingReadStatusArticleIDs { result in - switch result { - case .success(let pendingArticleIds): - self.remoteEntryIds.subtract(pendingArticleIds) - - self.updateUnreadStatuses() - - case .failure(let error): - self.didFinish(with: error) + MainActor.assumeIsolated { + switch result { + case .success(let pendingArticleIds): + self.remoteEntryIds.subtract(pendingArticleIds) + + self.updateUnreadStatuses() + + case .failure(let error): + self.didFinish(with: error) + } } } } @@ -98,12 +100,14 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { } account.fetchUnreadArticleIDs { result in - switch result { - case .success(let localUnreadArticleIDs): - self.processUnreadArticleIDs(localUnreadArticleIDs) - - case .failure(let error): - self.didFinish(with: error) + MainActor.assumeIsolated { + switch result { + case .success(let localUnreadArticleIDs): + self.processUnreadArticleIDs(localUnreadArticleIDs) + + case .failure(let error): + self.didFinish(with: error) + } } } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift index 814398121..0241daade 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyOperation.swift @@ -18,7 +18,7 @@ protocol FeedlyOperationDelegate: AnyObject { /// /// Normally we don’t do inheritance — but in this case /// it’s the best option. -class FeedlyOperation: MainThreadOperation { +@MainActor class FeedlyOperation: MainThreadOperation { weak var delegate: FeedlyOperationDelegate? var downloadProgress: DownloadProgress? { diff --git a/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift index e5d71060d..130cc700d 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift @@ -29,16 +29,18 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation { os_log(.debug, log: log, "Sending article statuses...") database.selectForProcessing { result in - if self.isCanceled { - self.didFinish() - return - } - - switch result { - case .success(let syncStatuses): - self.processStatuses(syncStatuses) - case .failure: - self.didFinish() + MainActor.assumeIsolated { + if self.isCanceled { + self.didFinish() + return + } + + switch result { + case .success(let syncStatuses): + self.processStatuses(syncStatuses) + case .failure: + self.didFinish() + } } } } diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift index 9c597ca12..50a28c81a 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift @@ -28,13 +28,15 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation { let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { databaseError in - if let error = databaseError { - self.didFinish(with: error) - return + MainActor.assumeIsolated { + if let error = databaseError { + self.didFinish(with: error) + return + } + + os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", feedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName) + self.didFinish() } - - os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", feedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName) - self.didFinish() } } } diff --git a/Core/Sources/Core/MainThreadOperation.swift b/Core/Sources/Core/MainThreadOperation.swift index 92a4f97c3..9b38ba70b 100644 --- a/Core/Sources/Core/MainThreadOperation.swift +++ b/Core/Sources/Core/MainThreadOperation.swift @@ -75,15 +75,15 @@ public protocol MainThreadOperation: AnyObject { public extension MainThreadOperation { - func cancel() { + @MainActor func cancel() { operationDelegate?.cancelOperation(self) } - func addDependency(_ parentOperation: MainThreadOperation) { + @MainActor func addDependency(_ parentOperation: MainThreadOperation) { operationDelegate?.make(self, dependOn: parentOperation) } - func informOperationDelegateOfCompletion() { + @MainActor func informOperationDelegateOfCompletion() { guard !isCanceled else { return } diff --git a/Core/Sources/Core/MainThreadOperationQueue.swift b/Core/Sources/Core/MainThreadOperationQueue.swift index cb1e091bc..dba872bd5 100644 --- a/Core/Sources/Core/MainThreadOperationQueue.swift +++ b/Core/Sources/Core/MainThreadOperationQueue.swift @@ -9,9 +9,10 @@ import Foundation public protocol MainThreadOperationDelegate: AnyObject { - func operationDidComplete(_ operation: MainThreadOperation) - func cancelOperation(_ operation: MainThreadOperation) - func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) + + @MainActor func operationDidComplete(_ operation: MainThreadOperation) + @MainActor func cancelOperation(_ operation: MainThreadOperation) + @MainActor func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) } /// Manage a queue of MainThreadOperation tasks. @@ -23,7 +24,7 @@ public protocol MainThreadOperationDelegate: AnyObject { /// Use this only on the main thread. /// The operation can be suspended and resumed. /// It is *not* suspended on creation. -public final class MainThreadOperationQueue { +@MainActor public final class MainThreadOperationQueue { /// Use the shared queue when you don’t need to create a separate queue. @MainActor public static let shared: MainThreadOperationQueue = { @@ -46,10 +47,6 @@ public final class MainThreadOperationQueue { // Silence compiler complaint about init not being public. } - deinit { - cancelAllOperations() - } - /// Add an operation to the queue. @MainActor public func add(_ operation: MainThreadOperation) { precondition(Thread.isMainThread) @@ -132,7 +129,7 @@ public final class MainThreadOperationQueue { extension MainThreadOperationQueue: MainThreadOperationDelegate { - public func operationDidComplete(_ operation: MainThreadOperation) { + @MainActor public func operationDidComplete(_ operation: MainThreadOperation) { precondition(Thread.isMainThread) operationDidFinish(operation) }