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
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))
}
}
}
}

View File

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

View File

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

View File

@ -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<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
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<Void, Error>) -> ()) {
func importOpml(_ opmlData: Data, completion: @escaping @Sendable (Result<Void, Error>) -> ()) {
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))

View File

@ -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<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 {
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.")
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)
}
}
}
}

View File

@ -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>) -> ())
}

View File

@ -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!

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")
@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 {

View File

@ -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)
}
}