Continue adopting MainActor.

This commit is contained in:
Brent Simmons
2023-07-08 15:18:57 -07:00
parent 7b947b7c9f
commit f7afdfc6c4
27 changed files with 270 additions and 272 deletions

View File

@@ -61,7 +61,7 @@ public enum FetchType {
case searchWithArticleIDs(String, Set<String>)
}
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
@MainActor public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
public struct UserInfoKey {
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
@@ -1017,13 +1017,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
nonisolated public func hash(into hasher: inout Hasher) {
hasher.combine(accountID)
}
// MARK: - Equatable
public class func ==(lhs: Account, rhs: Account) -> Bool {
nonisolated public class func ==(lhs: Account, rhs: Account) -> Bool {
return lhs === rhs
}
}

View File

@@ -11,7 +11,7 @@ import Articles
import RSWeb
import Secrets
protocol AccountDelegate {
@MainActor protocol AccountDelegate {
var behaviors: AccountBehaviors { get }

View File

@@ -9,30 +9,54 @@
import Foundation
import RSWeb
public struct WrappedAccountError: LocalizedError {
public let accountID: String
public let underlyingError: Error
public let isCredentialsError: Bool
public var errorTitle: String {
NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error")
}
public var errorDescription: String? {
if isCredentialsError {
let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.")
return String(format: localizedText, accountNameForDisplay)
}
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
return String(format: localizedText, accountNameForDisplay, underlyingError.localizedDescription)
}
public var recoverySuggestion: String? {
if isCredentialsError {
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
}
return NSLocalizedString("Please try again later.", comment: "Try later")
}
private let accountNameForDisplay: String
@MainActor init(account: Account, underlyingError: Error) {
self.accountID = account.accountID
self.underlyingError = underlyingError
self.accountNameForDisplay = account.nameForDisplay
var isCredentialsError = false
if case TransportError.httpError(let status) = underlyingError {
isCredentialsError = (status == HTTPResponseCode.unauthorized || status == HTTPResponseCode.forbidden)
}
self.isCredentialsError = isCredentialsError
}
}
public enum AccountError: LocalizedError {
case createErrorNotFound
case createErrorAlreadySubscribed
case opmlImportInProgress
case wrappedError(error: Error, account: Account)
public var account: Account? {
if case .wrappedError(_, let account) = self {
return account
} else {
return nil
}
}
public var isCredentialsError: Bool {
if case .wrappedError(let error, _) = self {
if case TransportError.httpError(let status) = error {
return isCredentialsError(status: status)
}
}
return false
}
public var errorTitle: String {
switch self {
case .createErrorNotFound:
@@ -41,8 +65,6 @@ public enum AccountError: LocalizedError {
return NSLocalizedString("error.title.already-subscribed", bundle: Bundle.module, comment: "Already Subscribed")
case .opmlImportInProgress:
return NSLocalizedString("error.title.ompl-import-in-progress", bundle: Bundle.module, comment: "OPML Import in Progress")
case .wrappedError(_, _):
return NSLocalizedString("error.title.error", bundle: Bundle.module, comment: "Error")
}
}
@@ -54,18 +76,6 @@ public enum AccountError: LocalizedError {
return NSLocalizedString("error.message.feed-already-subscribed", bundle: Bundle.module, comment: "You are already subscribed to this feed and cant add it again.")
case .opmlImportInProgress:
return NSLocalizedString("error.message.opml-import-in-progress", bundle: Bundle.module, comment: "An OPML import for this account is already running.")
case .wrappedError(let error, let account):
switch error {
case TransportError.httpError(let status):
if isCredentialsError(status: status) {
let localizedText = NSLocalizedString("error.message.credentials-expired.%@", bundle: Bundle.module, comment: "Your ”%@” credentials have expired.")
return String(format: localizedText, account.nameForDisplay)
} else {
return unknownError(error, account)
}
default:
return unknownError(error, account)
}
}
}
@@ -75,35 +85,8 @@ public enum AccountError: LocalizedError {
return nil
case .createErrorAlreadySubscribed:
return nil
case .wrappedError(let error, _):
switch error {
case TransportError.httpError(let status):
if isCredentialsError(status: status) {
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
} else {
return NSLocalizedString("Please try again later.", comment: "Try later")
}
default:
return NSLocalizedString("Please try again later.", comment: "Try later")
}
default:
return NSLocalizedString("Please try again later.", comment: "Try later")
}
}
}
// MARK: Private
private extension AccountError {
func unknownError(_ error: Error, _ account: Account) -> String {
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay, error.localizedDescription) as String
}
func isCredentialsError(status: Int) -> Bool {
return status == 401 || status == 403
}
}

View File

@@ -15,7 +15,7 @@ import RSDatabase
// Main thread only.
public final class AccountManager: UnreadCountProvider {
@MainActor public final class AccountManager: UnreadCountProvider {
public static var shared: AccountManager!
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"

View File

@@ -9,7 +9,7 @@
import Foundation
import RSCore
final class AccountMetadataFile: Logging {
@MainActor final class AccountMetadataFile: Logging {
private let fileURL: URL
private let account: Account

View File

@@ -10,7 +10,7 @@ import Foundation
import Articles
import ArticlesDatabase
public protocol ArticleFetcher {
@MainActor public protocol ArticleFetcher {
func fetchArticles() throws -> Set<Article>
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock)

View File

@@ -88,7 +88,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
/// Persist a web feed record to iCloud and return the external key
func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
@MainActor func createFeed(url: String, name: String?, editedName: String?, homePageURL: String?, container: Container, completion: @escaping (Result<String, Error>) -> Void) {
let recordID = CKRecord.ID(recordName: url.md5String, zoneID: zoneID)
let record = CKRecord(recordType: CloudKitFeed.recordType, recordID: recordID)
record[CloudKitFeed.Fields.url] = url
@@ -138,7 +138,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
@MainActor func removeFeed(_ feed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
guard let fromContainerExternalID = from.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount))
return
@@ -187,7 +187,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount))
return
@@ -209,7 +209,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func addFeed(_ feed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let toContainerExternalID = to.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount))
return
@@ -230,7 +230,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
@MainActor func findFeedExternalIDs(for folder: Folder, completion: @escaping (Result<[String], Error>) -> Void) {
guard let folderExternalID = folder.externalID else {
completion(.failure(CloudKitAccountZoneError.unknown))
return
@@ -292,7 +292,7 @@ final class CloudKitAccountZone: CloudKitZone {
createContainer(name: name, isAccount: false, completion: completion)
}
func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let externalID = folder.externalID else {
completion(.failure(CloudKitZoneError.corruptAccount))
return
@@ -312,7 +312,7 @@ final class CloudKitAccountZone: CloudKitZone {
}
}
func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
delete(externalID: folder.externalID, completion: completion)
}

View File

@@ -31,7 +31,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
self.articlesZone = articlesZone
}
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
for deletedRecordKey in deleted {
switch deletedRecordKey.recordType {
case CloudKitAccountZone.CloudKitFeed.recordType:
@@ -57,7 +57,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
completion(.success(()))
}
func addOrUpdateFeed(_ record: CKRecord) {
@MainActor func addOrUpdateFeed(_ record: CKRecord) {
guard let account = account,
let urlString = record[CloudKitAccountZone.CloudKitFeed.Fields.url] as? String,
let containerExternalIDs = record[CloudKitAccountZone.CloudKitFeed.Fields.containerExternalIDs] as? [String],
@@ -82,7 +82,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
}
func removeFeed(_ externalID: String) {
@MainActor func removeFeed(_ externalID: String) {
if let feed = account?.existingFeed(withExternalID: externalID), let containers = account?.existingContainers(withFeed: feed) {
containers.forEach {
feed.dropConditionalGetInfo()
@@ -91,7 +91,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
}
func addOrUpdateContainer(_ record: CKRecord) {
@MainActor func addOrUpdateContainer(_ record: CKRecord) {
guard let account = account,
let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String,
let isAccount = record[CloudKitAccountZone.CloudKitContainer.Fields.isAccount] as? String,
@@ -130,7 +130,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
}
}
func removeContainer(_ externalID: String) {
@MainActor func removeContainer(_ externalID: String) {
if let folder = account?.existingFolder(withExternalID: externalID) {
account?.removeFolder(folder)
}
@@ -140,7 +140,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private extension CloudKitAcountZoneDelegate {
func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
@MainActor func updateFeed(_ feed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
guard let account = account else { return }
feed.name = name
@@ -168,7 +168,7 @@ private extension CloudKitAcountZoneDelegate {
}
}
func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) {
@MainActor func createFeedIfNecessary(url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String, container: Container) {
guard let account = account else { return }
if account.existingFeed(withExternalID: feedExternalID) != nil {

View File

@@ -62,19 +62,22 @@ final class CloudKitArticlesZone: CloudKitZone {
migrateChangeToken()
}
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
}
var records = [CKRecord]()
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
for saveArticle in saveArticles {
records.append(makeStatusRecord(saveArticle))
records.append(makeArticleRecord(saveArticle))
}
@MainActor func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
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)
@@ -88,7 +91,7 @@ final class CloudKitArticlesZone: CloudKitZone {
delete(ckQuery: ckQuery, completion: completion)
}
func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !statusUpdates.isEmpty else {
completion(.success(()))
return
@@ -114,12 +117,16 @@ final class CloudKitArticlesZone: CloudKitZone {
}
}
let modifyRecordsCopy = modifyRecords
let newRecordsCopy = newRecords
let deleteRecordIDsCopy = deleteRecordIDs
compressionQueue.async {
let compressedModifyRecords = self.compressArticleRecords(modifyRecords)
self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in
let compressedModifyRecords = self.compressArticleRecords(modifyRecordsCopy)
self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDsCopy) { result in
switch result {
case .success:
let compressedNewRecords = self.compressArticleRecords(newRecords)
let compressedNewRecords = self.compressArticleRecords(newRecordsCopy)
self.saveIfNew(compressedNewRecords) { result in
switch result {
case .success:
@@ -143,12 +150,14 @@ private extension CloudKitArticlesZone {
func handleModifyArticlesError(_ error: Error, statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.modifyArticles(statusUpdates, completion: completion)
case .failure(let error):
completion(.failure(error))
}
Task { @MainActor in
switch result {
case .success:
self.modifyArticles(statusUpdates, completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
}
} else {
completion(.failure(error))
@@ -163,7 +172,7 @@ private extension CloudKitArticlesZone {
return "a|\(id)"
}
func makeStatusRecord(_ article: Article) -> CKRecord {
@MainActor func makeStatusRecord(_ article: Article) -> CKRecord {
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
if let feedExternalID = article.feed?.externalID {
@@ -174,7 +183,7 @@ private extension CloudKitArticlesZone {
return record
}
func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
@MainActor func makeStatusRecord(_ statusUpdate: CloudKitArticleStatusUpdate) -> CKRecord {
let recordID = CKRecord.ID(recordName: statusID(statusUpdate.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
@@ -188,7 +197,7 @@ private extension CloudKitArticlesZone {
return record
}
func makeArticleRecord(_ article: Article) -> CKRecord {
@MainActor func makeArticleRecord(_ article: Article) -> CKRecord {
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: zoneID)
let record = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)

View File

@@ -29,38 +29,37 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
}
func cloudKitWasChanged(updated: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
database.selectPendingReadStatusArticleIDs() { result in
switch result {
case .success(let pendingReadStatusArticleIDs):
self.database.selectPendingStarredStatusArticleIDs() { result in
switch result {
case .success(let pendingStarredStatusArticleIDs):
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
if let error = error {
completion(.failure(error))
} else {
self.update(records: updated,
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
completion: completion)
}
}
case .failure(let error):
database.selectPendingReadStatusArticleIDs() { result in
switch result {
case .success(let pendingReadStatusArticleIDs):
self.database.selectPendingStarredStatusArticleIDs() { result in
switch result {
case .success(let pendingStarredStatusArticleIDs):
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
Task { @MainActor in
if let error = error {
completion(.failure(error))
} else {
self.update(records: updated,
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
completion: completion)
}
}
}
case .failure(let error):
self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)")
completion(.failure(CloudKitZoneError.unknown))
}
}
case .failure(let error):
completion(.failure(CloudKitZoneError.unknown))
}
}
case .failure(let error):
self.logger.error("Error occurred getting pending read status records: \(error.localizedDescription, privacy: .public)")
completion(.failure(CloudKitZoneError.unknown))
}
}
}
completion(.failure(CloudKitZoneError.unknown))
}
}
}
}
private extension CloudKitArticlesZoneDelegate {
@@ -76,21 +75,23 @@ private extension CloudKitArticlesZoneDelegate {
}
database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in
if let databaseError = databaseError {
completion(databaseError)
} else {
self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
if let databaseError = databaseError {
completion(databaseError)
} else {
completion(nil)
}
}
}
Task { @MainActor in
if let databaseError = databaseError {
completion(databaseError)
} else {
self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
if let databaseError = databaseError {
completion(databaseError)
} else {
completion(nil)
}
}
}
}
}
}
func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) }))
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) }))

View File

@@ -71,7 +71,7 @@ private extension CloudKitSendStatusOperation {
switch result {
case .success(let syncStatuses):
func stopProcessing() {
@MainActor func stopProcessing() {
if self.showProgress {
self.refreshProgress?.completeTask()
}
@@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation {
let articleIDs = syncStatuses.map({ $0.articleID })
account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in
func processWithArticles(_ articles: Set<Article>) {
@MainActor func processWithArticles(_ articles: Set<Article>) {
let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID })
let articlesDict = articles.reduce(into: [String: Article]()) { result, article in
@@ -118,7 +118,7 @@ private extension CloudKitSendStatusOperation {
return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key])
}
func done(_ stop: Bool) {
func done(_ stop: Bool) {
// Don't clear the last one since we might have had additional ticks added
if self.showProgress && self.refreshProgress?.numberRemaining ?? 0 > 1 {
self.refreshProgress?.completeTask()

View File

@@ -18,35 +18,35 @@ extension Notification.Name {
public protocol Container: AnyObject, ContainerIdentifiable {
var account: Account? { get }
var topLevelFeeds: Set<Feed> { get set }
var folders: Set<Folder>? { get set }
var externalID: String? { get set }
@MainActor var account: Account? { get }
@MainActor var topLevelFeeds: Set<Feed> { get set }
@MainActor var folders: Set<Folder>? { get set }
@MainActor var externalID: String? { get set }
func hasAtLeastOneFeed() -> Bool
func objectIsChild(_ object: AnyObject) -> Bool
@MainActor func hasAtLeastOneFeed() -> Bool
@MainActor func objectIsChild(_ object: AnyObject) -> Bool
func hasChildFolder(with: String) -> Bool
func childFolder(with: String) -> Folder?
@MainActor func hasChildFolder(with: String) -> Bool
@MainActor func childFolder(with: String) -> Folder?
func removeFeed(_ feed: Feed)
func addFeed(_ feed: Feed)
@MainActor func removeFeed(_ feed: Feed)
@MainActor func addFeed(_ feed: Feed)
//Recursive  checks subfolders
func flattenedFeeds() -> Set<Feed>
func has(_ feed: Feed) -> Bool
func hasFeed(with feedID: String) -> Bool
func hasFeed(withURL url: String) -> Bool
func existingFeed(withFeedID: String) -> Feed?
func existingFeed(withURL url: String) -> Feed?
func existingFeed(withExternalID externalID: String) -> Feed?
func existingFolder(with name: String) -> Folder?
func existingFolder(withID: Int) -> Folder?
@MainActor func flattenedFeeds() -> Set<Feed>
@MainActor func has(_ feed: Feed) -> Bool
@MainActor func hasFeed(with feedID: String) -> Bool
@MainActor func hasFeed(withURL url: String) -> Bool
@MainActor func existingFeed(withFeedID: String) -> Feed?
@MainActor func existingFeed(withURL url: String) -> Feed?
@MainActor func existingFeed(withExternalID externalID: String) -> Feed?
@MainActor func existingFolder(with name: String) -> Folder?
@MainActor func existingFolder(withID: Int) -> Folder?
func postChildrenDidChangeNotification()
@MainActor func postChildrenDidChangeNotification()
}
public extension Container {
@MainActor public extension Container {
func hasAtLeastOneFeed() -> Bool {
return topLevelFeeds.count > 0

View File

@@ -9,10 +9,10 @@
import Foundation
public protocol ContainerIdentifiable {
var containerID: ContainerIdentifier? { get }
@MainActor var containerID: ContainerIdentifier? { get }
}
public enum ContainerIdentifier: Hashable, Equatable {
@MainActor public enum ContainerIdentifier: Hashable, Equatable {
case smartFeedController
case account(String) // accountID
case folder(String, String) // accountID, folderName

View File

@@ -12,7 +12,7 @@ import Foundation
// Mainly used with deleting objects and undo/redo.
// Especially redo. The idea is to put something back in the right place.
public struct ContainerPath {
@MainActor public struct ContainerPath {
private weak var account: Account?
private let names: [String] // empty if top-level of account

View File

@@ -48,7 +48,7 @@ extension Feed {
public extension Article {
var account: Account? {
@MainActor var account: Account? {
// The force unwrapped shared instance was crashing Account.framework unit tests.
guard let manager = AccountManager.shared else {
return nil
@@ -56,7 +56,7 @@ public extension Article {
return manager.existingAccount(with: accountID)
}
var feed: Feed? {
@MainActor var feed: Feed? {
return account?.existingFeed(withFeedID: feedID)
}
}

View File

@@ -210,7 +210,7 @@ public final class Feed: FeedProtocol, Renamable, Hashable, ObservableObject {
// MARK: - Renamable
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let account = account else { return }
account.renameFeed(self, to: newName, completion: completion)
}

View File

@@ -9,7 +9,7 @@
import Foundation
import RSCore
final class FeedMetadataFile: Logging {
@MainActor final class FeedMetadataFile: Logging {
private let fileURL: URL
private let account: Account

View File

@@ -19,7 +19,7 @@ public enum FeedbinAccountDelegateError: String, Error {
case unknown = "An unknown error occurred."
}
final class FeedbinAccountDelegate: AccountDelegate, Logging {
@MainActor final class FeedbinAccountDelegate: AccountDelegate, Logging {
private let database: SyncDatabase
@@ -92,7 +92,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -101,7 +101,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -134,7 +134,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
@@ -280,7 +280,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
self.refreshProgress.completeTask()
self.isOPMLImportInProgress = false
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -315,7 +315,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -409,7 +409,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -437,7 +437,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -484,7 +484,7 @@ final class FeedbinAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -1163,43 +1163,46 @@ private extension FeedbinAccountDelegate {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
func process(_ fetchedArticleIDs: Set<String>) {
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
let group = DispatchGroup()
var errorOccurred = false
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 100)
for chunk in chunkedArticleIDs {
group.enter()
self.caller.retrieveEntries(articleIDs: chunk) { result in
for chunk in chunkedArticleIDs {
group.enter()
self.caller.retrieveEntries(articleIDs: chunk) { result in
switch result {
case .success(let entries):
// Task { @MainActor in
switch result {
case .success(let entries):
self.processEntries(account: account, entries: entries) { error in
group.leave()
if error != nil {
errorOccurred = true
}
}
self.processEntries(account: account, entries: entries) { error in
group.leave()
if error != nil {
errorOccurred = true
}
}
case .failure(let error):
errorOccurred = true
self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)")
group.leave()
}
}
}
case .failure(let error):
errorOccurred = true
self.logger.error("Refreshing missing articles failed: \(error.localizedDescription, privacy: .public)")
group.leave()
}
// }
}
}
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.completeTask()
self.logger.debug("Done refreshing missing articles.")
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
// Task { @MainActor in
self.refreshProgress.completeTask()
self.logger.debug("Done refreshing missing articles.")
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
// }
}
}
@@ -1273,7 +1276,7 @@ private extension FeedbinAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
@@ -1326,7 +1329,7 @@ private extension FeedbinAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
@@ -1386,7 +1389,7 @@ private extension FeedbinAccountDelegate {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}

View File

@@ -236,7 +236,7 @@ final class FeedlyAccountDelegate: AccountDelegate, Logging {
self.refreshProgress.completeTask()
self.isOPMLImportInProgress = false
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}

View File

@@ -11,7 +11,7 @@ import Foundation
struct FeedlyFeedContainerValidator {
var container: Container
func getValidContainer() throws -> (Folder, String) {
@MainActor func getValidContainer() throws -> (Folder, String) {
guard let folder = container as? Folder else {
throw FeedlyAccountDelegateError.addFeedChooseFolder
}

View File

@@ -16,7 +16,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
return .read
}
public var containerID: ContainerIdentifier? {
@MainActor public var containerID: ContainerIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
@@ -24,7 +24,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
return ContainerIdentifier.folder(accountID, nameForDisplay)
}
public var itemID: ItemIdentifier? {
@MainActor public var itemID: ItemIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
@@ -66,7 +66,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
// MARK: - Renamable
public func rename(to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor public func rename(to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let account = account else { return }
account.renameFolder(self, to: name, completion: completion)
}
@@ -122,7 +122,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
postChildrenDidChangeNotification()
}
public func addFeeds(_ feeds: Set<Feed>) {
@MainActor public func addFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else {
return
}
@@ -135,7 +135,7 @@ public final class Folder: FeedProtocol, Renamable, Container, Hashable {
postChildrenDidChangeNotification()
}
public func removeFeeds(_ feeds: Set<Feed>) {
@MainActor public func removeFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else {
return
}
@@ -168,7 +168,7 @@ private extension Folder {
unreadCount = updatedUnreadCount
}
func childrenContain(_ feed: Feed) -> Bool {
@MainActor func childrenContain(_ feed: Feed) -> Bool {
return topLevelFeeds.contains(feed)
}
}
@@ -177,7 +177,7 @@ private extension Folder {
extension Folder: OPMLRepresentable {
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
@MainActor public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
let attrExternalID: String = {
if allowCustomAttributes, let externalID = externalID {
@@ -220,7 +220,7 @@ extension Folder: OPMLRepresentable {
// MARK: Set
extension Set where Element == Folder {
@MainActor extension Set where Element == Folder {
func sorted() -> Array<Folder> {
return sorted(by: { (folder1, folder2) -> Bool in

View File

@@ -93,29 +93,30 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
let parserData = ParserData(url: feed.url, data: data)
FeedParser.parse(parserData) { (parsedFeed, error) in
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
account.update(feed, with: parsedFeed) { result in
if case .success(let articleChanges) = result {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
completion()
}
} else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
}
Task { @MainActor in
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
account.update(feed, with: parsedFeed) { result in
if case .success(let articleChanges) = result {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
completion()
}
} else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
}
}
}
}

View File

@@ -323,7 +323,7 @@ extension NewsBlurAccountDelegate {
}
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
@@ -371,7 +371,8 @@ extension NewsBlurAccountDelegate {
}
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
@@ -550,7 +551,7 @@ extension NewsBlurAccountDelegate {
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}

View File

@@ -90,7 +90,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -134,7 +134,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
logger.debug("Sending story statuses...")
database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter {
$0.key == SyncStatus.Key.read && $0.flag == false
}
@@ -270,7 +270,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
func process(_ fetchedHashes: Set<String>) {
@MainActor func process(_ fetchedHashes: Set<String>) {
let group = DispatchGroup()
var errorOccurred = false
@@ -432,7 +432,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
self.createFeed(account: account, newsBlurFeed: feed, name: name, container: container, completion: completion)
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -459,7 +459,7 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}

View File

@@ -10,7 +10,7 @@ import Foundation
import RSCore
import RSParser
final class OPMLFile: Logging {
@MainActor final class OPMLFile: Logging {
private let fileURL: URL
private let account: Account

View File

@@ -33,7 +33,7 @@ public enum ReaderAPIAccountDelegateError: LocalizedError {
}
}
final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
@MainActor final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
private let variant: ReaderAPIVariant
@@ -135,7 +135,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
if wrappedError.isCredentialsError, let basicCredentials = try? account.retrieveCredentials(type: .readerBasic), let endpoint = account.endpointURL {
self.caller.credentials = basicCredentials
@@ -198,7 +198,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
@@ -312,7 +312,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -422,7 +422,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -456,7 +456,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -487,7 +487,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -537,7 +537,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate, Logging {
}
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
let wrappedError = WrappedAccountError(account: account, underlyingError: error)
completion(.failure(wrappedError))
}
}
@@ -980,7 +980,7 @@ private extension ReaderAPIAccountDelegate {
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
func process(_ fetchedArticleIDs: Set<String>) {
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
guard !fetchedArticleIDs.isEmpty else {
completion()
return
@@ -1086,7 +1086,7 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in
@@ -1135,7 +1135,7 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in

View File

@@ -255,7 +255,7 @@ final class ReaderAPICaller: NSObject {
}
}
func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func deleteTag(folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
guard let baseURL = apiBaseURL else {
completion(.failure(CredentialsError.incompleteCredentials))
return