Fix numerous concurrency warnings.

This commit is contained in:
Brent Simmons 2024-04-07 22:15:35 -07:00
parent 52345724ce
commit dfcf567270
13 changed files with 111 additions and 79 deletions

View File

@ -271,7 +271,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
} }
} }
private func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) { private func importOPML(for account: Account, opmlFile: URL, completion: @escaping @Sendable (Result<Void, Error>) -> Void) {
let data: Data let data: Data
do { do {
@ -286,21 +286,24 @@ final class FeedlyAccountDelegate: AccountDelegate {
refreshProgress.addToNumberOfTasksAndRemaining(1) refreshProgress.addToNumberOfTasksAndRemaining(1)
caller.importOpml(data) { result in caller.importOpml(data) { result in
switch result {
case .success: MainActor.assumeIsolated {
os_log(.debug, log: self.log, "Import OPML done.") switch result {
self.refreshProgress.completeTask() case .success:
self.isOPMLImportInProgress = false os_log(.debug, log: self.log, "Import OPML done.")
DispatchQueue.main.async { self.refreshProgress.completeTask()
completion(.success(())) self.isOPMLImportInProgress = false
} DispatchQueue.main.async {
case .failure(let error): completion(.success(()))
os_log(.debug, log: self.log, "Import OPML failed.") }
self.refreshProgress.completeTask() case .failure(let error):
self.isOPMLImportInProgress = false os_log(.debug, log: self.log, "Import OPML failed.")
DispatchQueue.main.async { self.refreshProgress.completeTask()
let wrappedError = AccountError.wrappedError(error: error, account: account) self.isOPMLImportInProgress = false
completion(.failure(wrappedError)) DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
completion(.failure(wrappedError))
}
} }
} }
} }

View File

@ -8,7 +8,7 @@
import Articles import Articles
import Database import Database
@preconcurrency import Parser import Parser
import Web import Web
import SyncDatabase import SyncDatabase
import os.log import os.log

View File

@ -9,7 +9,8 @@
import Foundation import Foundation
public protocol ContainerIdentifiable { public protocol ContainerIdentifiable {
var containerID: ContainerIdentifier? { get }
@MainActor var containerID: ContainerIdentifier? { get }
} }
public enum ContainerIdentifier: Hashable, Equatable, Sendable { public enum ContainerIdentifier: Hashable, Equatable, Sendable {

View File

@ -14,7 +14,7 @@ import Feedly
protocol FeedlyAPICallerDelegate: AnyObject { protocol FeedlyAPICallerDelegate: AnyObject {
/// Implemented by the `FeedlyAccountDelegate` reauthorize the client with a fresh OAuth token so the client can retry the unauthorized request. /// 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. /// 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 { final class FeedlyAPICaller {
@ -86,55 +86,57 @@ final class FeedlyAPICaller {
isSuspended = false isSuspended = false
} }
func send<R: Decodable>(request: URLRequest, resultType: R.Type, dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601, keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) { func send<R: Decodable & Sendable>(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 transport.send(request: request, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding) { [weak self] result in
assert(Thread.isMainThread)
switch result { MainActor.assumeIsolated {
case .success:
completion(result) switch result {
case .failure(let error): case .success:
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) 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<Void, Error>) -> ()) { func importOpml(_ opmlData: Data, completion: @escaping @Sendable (Result<Void, Error>) -> ()) {
guard !isSuspended else { guard !isSuspended else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completion(.failure(TransportError.suspended)) completion(.failure(TransportError.suspended))
@ -566,7 +568,7 @@ extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting {
extension FeedlyAPICaller: FeedlyGetCollectionsService { extension FeedlyAPICaller: FeedlyGetCollectionsService {
func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) { func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ()) {
guard !isSuspended else { guard !isSuspended else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completion(.failure(TransportError.suspended)) completion(.failure(TransportError.suspended))

View File

@ -43,5 +43,5 @@ public protocol OAuthAcessTokenRefreshRequesting {
/// Implemented by concrete types to perform the actual request. /// Implemented by concrete types to perform the actual request.
protocol OAuthAccessTokenRefreshing: AnyObject { protocol OAuthAccessTokenRefreshing: AnyObject {
func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) @MainActor func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
} }

View File

@ -28,6 +28,7 @@ public enum CloudKitZoneError: LocalizedError {
} }
public protocol CloudKitZoneDelegate: AnyObject { public protocol CloudKitZoneDelegate: AnyObject {
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void); func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
} }

View File

@ -31,15 +31,18 @@ public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollect
os_log(.debug, log: log, "Requesting collections.") os_log(.debug, log: log, "Requesting collections.")
service.getCollections { result in service.getCollections { result in
switch result {
case .success(let collections): MainActor.assumeIsolated {
os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) switch result {
self.collections = collections case .success(let collections):
self.didFinish() os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id })
self.collections = collections
case .failure(let error): self.didFinish()
os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError)
self.didFinish(with: error) case .failure(let error):
os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError)
self.didFinish(with: error)
}
} }
} }
} }

View File

@ -9,5 +9,5 @@
import Foundation import Foundation
public protocol FeedlyGetCollectionsService: AnyObject { public protocol FeedlyGetCollectionsService: AnyObject {
func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) func getCollections(completion: @escaping @Sendable (Result<[FeedlyCollection], Error>) -> ())
} }

View File

@ -10,7 +10,7 @@ import Cocoa
import os.log import os.log
import UniformTypeIdentifiers import UniformTypeIdentifiers
class ShareViewController: NSViewController { final class ShareViewController: NSViewController {
@IBOutlet weak var nameTextField: NSTextField! @IBOutlet weak var nameTextField: NSTextField!
@IBOutlet weak var folderPopUpButton: NSPopUpButton! @IBOutlet weak var folderPopUpButton: NSPopUpButton!

View File

@ -0,0 +1,10 @@
//
// File.swift
//
//
// Created by Brent Simmons on 4/7/24.
//
import Foundation
extension ParserData: @unchecked Sendable {}

View File

@ -0,0 +1,10 @@
//
// File.swift
//
//
// Created by Brent Simmons on 4/7/24.
//
import Foundation
extension RSHTMLMetadataParser: @unchecked Sendable {}

View File

@ -14,7 +14,7 @@ struct HTMLMetadataDownloader {
static let serialDispatchQueue = DispatchQueue(label: "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 { guard let actualURL = URL(unicodeString: url) else {
completion(nil) completion(nil)
return 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 { serialDispatchQueue.async {
let htmlMetadata = RSHTMLMetadataParser.htmlMetadata(with: parserData) let htmlMetadata = RSHTMLMetadataParser.htmlMetadata(with: parserData)
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -216,11 +216,13 @@ private extension FeedIconDownloader {
HTMLMetadataDownloader.downloadMetadata(for: homePageURL) { (metadata) in HTMLMetadataDownloader.downloadMetadata(for: homePageURL) { (metadata) in
self.urlsInProgress.remove(homePageURL) MainActor.assumeIsolated {
guard let metadata = metadata else { self.urlsInProgress.remove(homePageURL)
return guard let metadata = metadata else {
return
}
self.pullIconURL(from: metadata, homePageURL: homePageURL, feed: feed)
} }
self.pullIconURL(from: metadata, homePageURL: homePageURL, feed: feed)
} }
} }