diff --git a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift index 6a74a749b..e57eacf3d 100644 --- a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift @@ -271,7 +271,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } } - private func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { + private func importOPML(for account: Account, opmlFile: URL, completion: @escaping @Sendable (Result) -> Void) { let data: Data do { @@ -286,21 +286,24 @@ final class FeedlyAccountDelegate: AccountDelegate { refreshProgress.addToNumberOfTasksAndRemaining(1) caller.importOpml(data) { result in - switch result { - case .success: - os_log(.debug, log: self.log, "Import OPML done.") - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - completion(.success(())) - } - case .failure(let error): - os_log(.debug, log: self.log, "Import OPML failed.") - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + + MainActor.assumeIsolated { + switch result { + case .success: + os_log(.debug, log: self.log, "Import OPML done.") + self.refreshProgress.completeTask() + self.isOPMLImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML failed.") + self.refreshProgress.completeTask() + self.isOPMLImportInProgress = false + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } } } } diff --git a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift index c0a8f218a..0db0210fb 100644 --- a/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift @@ -8,7 +8,7 @@ import Articles import Database -@preconcurrency import Parser +import Parser import Web import SyncDatabase import os.log diff --git a/Account/Sources/Account/ContainerIdentifier.swift b/Account/Sources/Account/ContainerIdentifier.swift index 55dced932..e7dab9bc1 100644 --- a/Account/Sources/Account/ContainerIdentifier.swift +++ b/Account/Sources/Account/ContainerIdentifier.swift @@ -9,7 +9,8 @@ import Foundation public protocol ContainerIdentifiable { - var containerID: ContainerIdentifier? { get } + + @MainActor var containerID: ContainerIdentifier? { get } } public enum ContainerIdentifier: Hashable, Equatable, Sendable { diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index a76948e64..82a6a684a 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -14,7 +14,7 @@ import Feedly protocol FeedlyAPICallerDelegate: AnyObject { /// Implemented by the `FeedlyAccountDelegate` reauthorize the client with a fresh OAuth token so the client can retry the unauthorized request. /// Pass `true` to the completion handler if the failing request should be retried with a fresh token or `false` if the unauthorized request should complete with the original failure error. - func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) + @MainActor func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) } final class FeedlyAPICaller { @@ -86,55 +86,57 @@ final class FeedlyAPICaller { isSuspended = false } - func send(request: URLRequest, resultType: R.Type, dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601, keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) { + func send(request: URLRequest, resultType: R.Type, dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601, keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) { transport.send(request: request, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding) { [weak self] result in - assert(Thread.isMainThread) - switch result { - case .success: - completion(result) - case .failure(let error): - switch error { - case TransportError.httpError(let statusCode) where statusCode == 401: - - assert(self == nil ? true : self?.delegate != nil, "Check the delegate is set to \(FeedlyAccountDelegate.self).") - - guard let self = self, let delegate = self.delegate else { - completion(result) - return - } - - /// Capture the credentials before the reauthorization to check for a change. - let credentialsBefore = self.credentials - - delegate.reauthorizeFeedlyAPICaller(self) { [weak self] isReauthorizedAndShouldRetry in - assert(Thread.isMainThread) - - guard isReauthorizedAndShouldRetry, let self = self else { - completion(result) - return - } - - // Check for a change. Not only would it help debugging, but it'll also catch an infinitely recursive attempt to refresh. - guard let accessToken = self.credentials?.secret, accessToken != credentialsBefore?.secret else { - assertionFailure("Could not update the request with a new OAuth token. Did \(String(describing: self.delegate)) set them on \(self)?") - completion(result) - return - } - - var reauthorizedRequest = request - reauthorizedRequest.setValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - - self.send(request: reauthorizedRequest, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding, completion: completion) - } - default: + MainActor.assumeIsolated { + + switch result { + case .success: completion(result) + case .failure(let error): + switch error { + case TransportError.httpError(let statusCode) where statusCode == 401: + + assert(self == nil ? true : self?.delegate != nil, "Check the delegate is set to \(FeedlyAccountDelegate.self).") + + guard let self = self, let delegate = self.delegate else { + completion(result) + return + } + + /// Capture the credentials before the reauthorization to check for a change. + let credentialsBefore = self.credentials + + delegate.reauthorizeFeedlyAPICaller(self) { [weak self] isReauthorizedAndShouldRetry in + assert(Thread.isMainThread) + + guard isReauthorizedAndShouldRetry, let self = self else { + completion(result) + return + } + + // Check for a change. Not only would it help debugging, but it'll also catch an infinitely recursive attempt to refresh. + guard let accessToken = self.credentials?.secret, accessToken != credentialsBefore?.secret else { + assertionFailure("Could not update the request with a new OAuth token. Did \(String(describing: self.delegate)) set them on \(self)?") + completion(result) + return + } + + var reauthorizedRequest = request + reauthorizedRequest.setValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + self.send(request: reauthorizedRequest, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding, completion: completion) + } + default: + completion(result) + } } } } } - func importOpml(_ opmlData: Data, completion: @escaping (Result) -> ()) { + func importOpml(_ opmlData: Data, completion: @escaping @Sendable (Result) -> ()) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) @@ -566,7 +568,7 @@ extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting { extension FeedlyAPICaller: FeedlyGetCollectionsService { - func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) { + func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ()) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) diff --git a/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift b/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift index 0179a63f0..de7d21fbc 100644 --- a/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift +++ b/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift @@ -43,5 +43,5 @@ public protocol OAuthAcessTokenRefreshRequesting { /// Implemented by concrete types to perform the actual request. protocol OAuthAccessTokenRefreshing: AnyObject { - func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) + @MainActor func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) } diff --git a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift index ee86d5827..fce4e8df5 100644 --- a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift +++ b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift @@ -28,6 +28,7 @@ public enum CloudKitZoneError: LocalizedError { } public protocol CloudKitZoneDelegate: AnyObject { + func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result) -> Void); } diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift index 3ad8b7964..f9f0c1a7d 100644 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift +++ b/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift @@ -31,15 +31,18 @@ public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollect os_log(.debug, log: log, "Requesting collections.") service.getCollections { result in - switch result { - case .success(let collections): - os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) - self.collections = collections - self.didFinish() - - case .failure(let error): - os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) - self.didFinish(with: error) + + MainActor.assumeIsolated { + switch result { + case .success(let collections): + os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) + self.collections = collections + self.didFinish() + + case .failure(let error): + os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) + self.didFinish(with: error) + } } } } diff --git a/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift b/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift index e46a4a5b1..b1c47abfd 100644 --- a/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift +++ b/Feedly/Sources/Feedly/Services/FeedlyGetCollectionsService.swift @@ -9,5 +9,5 @@ import Foundation public protocol FeedlyGetCollectionsService: AnyObject { - func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) + func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ()) } diff --git a/Mac/ShareExtension/ShareViewController.swift b/Mac/ShareExtension/ShareViewController.swift index 85a58488f..1cd83d422 100644 --- a/Mac/ShareExtension/ShareViewController.swift +++ b/Mac/ShareExtension/ShareViewController.swift @@ -10,7 +10,7 @@ import Cocoa import os.log import UniformTypeIdentifiers -class ShareViewController: NSViewController { +final class ShareViewController: NSViewController { @IBOutlet weak var nameTextField: NSTextField! @IBOutlet weak var folderPopUpButton: NSPopUpButton! diff --git a/Parser/Sources/Swift/ParserData+Parser.swift b/Parser/Sources/Swift/ParserData+Parser.swift new file mode 100644 index 000000000..f9e9ca9ea --- /dev/null +++ b/Parser/Sources/Swift/ParserData+Parser.swift @@ -0,0 +1,10 @@ +// +// File.swift +// +// +// Created by Brent Simmons on 4/7/24. +// + +import Foundation + +extension ParserData: @unchecked Sendable {} diff --git a/Parser/Sources/Swift/RSHTMLMetadata+Parser.swift b/Parser/Sources/Swift/RSHTMLMetadata+Parser.swift new file mode 100644 index 000000000..391380b22 --- /dev/null +++ b/Parser/Sources/Swift/RSHTMLMetadata+Parser.swift @@ -0,0 +1,10 @@ +// +// File.swift +// +// +// Created by Brent Simmons on 4/7/24. +// + +import Foundation + +extension RSHTMLMetadataParser: @unchecked Sendable {} diff --git a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift b/Shared/HTMLMetadata/HTMLMetadataDownloader.swift index 910d47033..051ba744f 100644 --- a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift +++ b/Shared/HTMLMetadata/HTMLMetadataDownloader.swift @@ -14,7 +14,7 @@ struct HTMLMetadataDownloader { static let serialDispatchQueue = DispatchQueue(label: "HTMLMetadataDownloader") - @MainActor static func downloadMetadata(for url: String, _ completion: @escaping (RSHTMLMetadata?) -> Void) { + @MainActor static func downloadMetadata(for url: String, _ completion: @escaping @Sendable (RSHTMLMetadata?) -> Void) { guard let actualURL = URL(unicodeString: url) else { completion(nil) return @@ -32,7 +32,7 @@ struct HTMLMetadataDownloader { } } - private static func parseMetadata(with parserData: ParserData, _ completion: @escaping (RSHTMLMetadata?) -> Void) { + private static func parseMetadata(with parserData: ParserData, _ completion: @escaping @Sendable (RSHTMLMetadata?) -> Void) { serialDispatchQueue.async { let htmlMetadata = RSHTMLMetadataParser.htmlMetadata(with: parserData) DispatchQueue.main.async { diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index f379491e1..63ffd6ad3 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -216,11 +216,13 @@ private extension FeedIconDownloader { HTMLMetadataDownloader.downloadMetadata(for: homePageURL) { (metadata) in - self.urlsInProgress.remove(homePageURL) - guard let metadata = metadata else { - return + MainActor.assumeIsolated { + self.urlsInProgress.remove(homePageURL) + guard let metadata = metadata else { + return + } + self.pullIconURL(from: metadata, homePageURL: homePageURL, feed: feed) } - self.pullIconURL(from: metadata, homePageURL: homePageURL, feed: feed) } }