Mark many things as MainActor and deal with the fallout.

This commit is contained in:
Brent Simmons 2024-03-25 21:10:37 -07:00
parent 87db1e3d5f
commit 27d27cbf1a
34 changed files with 693 additions and 625 deletions

View File

@ -62,7 +62,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
@ -894,12 +894,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return
}
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
if let error = error {
completion?(error)
return
MainActor.assumeIsolated {
if let error = error {
completion?(error)
return
}
self.noteStatusesForArticleIDsDidChange(articleIDs)
completion?(nil)
}
self.noteStatusesForArticleIDsDidChange(articleIDs)
completion?(nil)
}
}
@ -912,11 +915,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return
}
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
if let error {
completion?(error)
} else {
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
completion?(nil)
MainActor.assumeIsolated {
if let error {
completion?(error)
} else {
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
completion?(nil)
}
}
}
}

View File

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

View File

@ -14,18 +14,22 @@ 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
case wrappedError(error: Error, accountID: String, accountName: String)
@MainActor public var account: Account? {
if case .wrappedError(_, let accountID, _) = self {
return AccountManager.shared.existingAccount(with: accountID)
} else {
return nil
}
}
public var isCredentialsError: Bool {
if case .wrappedError(let error, _) = self {
@MainActor public static func wrappedError(error: Error, account: Account) -> AccountError {
wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay)
}
@MainActor public var isCredentialsError: Bool {
if case .wrappedError(let error, _, _) = self {
if case TransportError.httpError(let status) = error {
return isCredentialsError(status: status)
}
@ -41,17 +45,17 @@ public enum AccountError: LocalizedError {
return NSLocalizedString("You are already subscribed to this feed and cant add it again.", comment: "Already subscribed")
case .opmlImportInProgress:
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
case .wrappedError(let error, let account):
case .wrappedError(let error, _, let accountName):
switch error {
case TransportError.httpError(let status):
if isCredentialsError(status: status) {
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
return NSString.localizedStringWithFormat(localizedText as NSString, account.nameForDisplay) as String
return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String
} else {
return unknownError(error, account)
return unknownError(error, accountName)
}
default:
return unknownError(error, account)
return unknownError(error, accountName)
}
}
}
@ -62,7 +66,7 @@ public enum AccountError: LocalizedError {
return nil
case .createErrorAlreadySubscribed:
return nil
case .wrappedError(let error, _):
case .wrappedError(let error, _, _):
switch error {
case TransportError.httpError(let status):
if isCredentialsError(status: status) {
@ -84,9 +88,9 @@ public enum AccountError: LocalizedError {
private extension AccountError {
func unknownError(_ error: Error, _ account: Account) -> String {
func unknownError(_ error: Error, _ accountName: String) -> 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
return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String
}
func isCredentialsError(status: Int) -> Bool {

View File

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

View File

@ -28,7 +28,7 @@ enum CloudKitAccountDelegateError: LocalizedError {
}
}
final class CloudKitAccountDelegate: AccountDelegate {
@MainActor final class CloudKitAccountDelegate: AccountDelegate {
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
@ -416,10 +416,12 @@ final class CloudKitAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account, showProgress: false) { _ in }
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account, showProgress: false) { _ in }
}
completion(.success(()))
}
completion(.success(()))
}
}
case .failure(let error):
@ -648,35 +650,36 @@ private extension CloudKitAccountDelegate {
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed) { result in
switch result {
case .success:
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
name: parsedFeed.title,
editedName: editedName,
homePageURL: parsedFeed.homePageURL,
container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
feed.externalID = externalID
self.sendNewArticlesToTheCloud(account, feed)
completion(.success(feed))
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(2)
completion(.failure(error))
MainActor.assumeIsolated {
switch result {
case .success:
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
name: parsedFeed.title,
editedName: editedName,
homePageURL: parsedFeed.homePageURL,
container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
feed.externalID = externalID
self.sendNewArticlesToTheCloud(account, feed)
completion(.success(feed))
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(2)
completion(.failure(error))
}
}
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(3)
completion(.failure(error))
}
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(3)
completion(.failure(error))
}
}
} else {
self.refreshProgress.completeTasks(3)
@ -700,9 +703,9 @@ private extension CloudKitAccountDelegate {
}
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
Task { @MainActor in
do {
let articles = try await account.articles(for: .feed(feed))
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
@ -716,7 +719,7 @@ private extension CloudKitAccountDelegate {
}
}
}
} catch {
os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription)
}

View File

@ -20,7 +20,8 @@ enum CloudKitAccountZoneError: LocalizedError {
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
}
}
final class CloudKitAccountZone: CloudKitZone {
@MainActor final class CloudKitAccountZone: CloudKitZone {
var zoneID: CKRecordZone.ID

View File

@ -13,8 +13,8 @@ import CloudKit
import Articles
import CloudKitExtras
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
@MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
private var existingUnclaimedFeeds = [String: [Feed]]()
@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
account?.removeFolder(folder)
}
}
}
private extension CloudKitAcountZoneDelegate {

View File

@ -108,7 +108,7 @@ private extension CloudKitSendStatusOperation {
}
}
func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
@MainActor func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
guard let account = account, let articlesZone = articlesZone else {
completion(true)
return
@ -156,9 +156,11 @@ private extension CloudKitSendStatusOperation {
}
case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
self.processAccountError(account, error)
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
completion(true)
MainActor.assumeIsolated {
self.processAccountError(account, error)
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription)
completion(true)
}
}
}
}
@ -178,7 +180,7 @@ private extension CloudKitSendStatusOperation {
}
}
func processAccountError(_ account: Account, _ error: Error) {
@MainActor func processAccountError(_ account: Account, _ error: Error) {
if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelFeeds)
for folder in account.folders ?? Set<Folder>() {

View File

@ -15,20 +15,20 @@ extension Notification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
}
public protocol Container: AnyObject, ContainerIdentifiable {
@MainActor 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 }
func hasAtLeastOneFeed() -> Bool
func objectIsChild(_ object: AnyObject) -> Bool
func hasChildFolder(with: String) -> Bool
func childFolder(with: String) -> Folder?
func removeFeed(_ feed: Feed)
func removeFeed(_ feed: Feed)
func addFeed(_ feed: Feed)
//Recursive  checks subfolders

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

@ -11,7 +11,7 @@ import RSWeb
import Articles
import Core
public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable {
@MainActor public final class Feed: Renamable, DisplayNameProvider, UnreadCountProvider, Hashable {
public weak var account: Account?
public let url: String
@ -294,11 +294,11 @@ extension Feed: OPMLRepresentable {
extension Set where Element == Feed {
func feedIDs() -> Set<String> {
@MainActor func feedIDs() -> Set<String> {
return Set<String>(map { $0.feedID })
}
func sorted() -> Array<Feed> {
@MainActor func sorted() -> Array<Feed> {
return sorted(by: { (feed1, feed2) -> Bool in
if feed1.nameForDisplay.localizedStandardCompare(feed2.nameForDisplay) == .orderedSame {
return feed1.url < feed2.url

View File

@ -136,62 +136,64 @@ final class FeedbinAccountDelegate: AccountDelegate {
database.selectForProcessing { result in
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 }
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup()
var errorOccurred = false
group.enter()
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
MainActor.assumeIsolated {
@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 }
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup()
var errorOccurred = false
group.enter()
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
group.enter()
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
group.enter()
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
}
}
@ -564,10 +566,12 @@ final class FeedbinAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
}
completion(.success(()))
}
completion(.success(()))
}
}
case .failure(let error):
@ -1082,43 +1086,46 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) { error in
MainActor.assumeIsolated {
if let error = error {
completion(.failure(error))
return
}
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
DispatchQueue.main.async {
completion(.success(feed))
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
DispatchQueue.main.async {
completion(.success(feed))
}
case .failure(let error):
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
@ -1145,21 +1152,24 @@ private extension FeedbinAccountDelegate {
}
self.processEntries(account: account, entries: entries) { error in
self.refreshProgress.completeTask()
if let error = error {
completion(.failure(error))
return
}
MainActor.assumeIsolated {
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
os_log(.debug, log: self.log, "Done refreshing articles.")
switch result {
case .success:
completion(.success(()))
case .failure(let error):
self.refreshProgress.completeTask()
if let error = error {
completion(.failure(error))
return
}
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
os_log(.debug, log: self.log, "Done refreshing articles.")
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
}
@ -1250,16 +1260,18 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, nextPage)):
self.processEntries(account: account, entries: entries) { error in
self.refreshProgress.completeTask()
MainActor.assumeIsolated {
self.refreshProgress.completeTask()
if let error = error {
completion(.failure(error))
return
if let error = error {
completion(.failure(error))
return
}
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
}
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
}
case .failure(let error):
completion(.failure(error))
}
@ -1294,47 +1306,50 @@ private extension FeedbinAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as unread
let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
let group = DispatchGroup()
// Mark articles as unread
let deltaUnreadArticleIDs = updatableFeedbinUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableFeedbinUnreadArticleIDs)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
}
}
}
@ -1347,46 +1362,50 @@ private extension FeedbinAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
// Mark articles as starred
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as starred
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
}
}
}

View File

@ -408,7 +408,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let folder = container as? Folder, let collectionId = folder.externalID else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay)))
}
}
@ -442,7 +442,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
completion(.success(()))
case .failure:
from.addFeed(feed)
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to)))
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
}
}
case .failure(let error):

View File

@ -14,12 +14,12 @@ enum FeedlyAccountDelegateError: LocalizedError {
case unableToAddFolder(String)
case unableToRenameFolder(String, String)
case unableToRemoveFolder(String)
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
case unableToMoveFeedBetweenFolders(String, String, String)
case addFeedChooseFolder
case addFeedInvalidFolder(Folder)
case addFeedInvalidFolder(String)
case unableToRenameFeed(String, String)
case unableToRemoveFeed(Feed)
case unableToRemoveFeed(String)
var errorDescription: String? {
switch self {
case .notLoggedIn:
@ -41,24 +41,24 @@ enum FeedlyAccountDelegateError: LocalizedError {
let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly Could not remove a folder/collection.")
return String(format: template, name)
case .unableToMoveFeedBetweenFolders(let feed, _, let to):
case .unableToMoveFeedBetweenFolders(let feedName, _, let destinationFolderName):
let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly Could not move a feed between folders/collections.")
return String(format: template, feed.nameForDisplay, to.nameForDisplay)
return String(format: template, feedName, destinationFolderName)
case .addFeedChooseFolder:
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly Feed can only be added to folders.")
case .addFeedInvalidFolder(let invalidFolder):
case .addFeedInvalidFolder(let folderName):
let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly Feed can only be added to folders.")
return String(format: template, invalidFolder.nameForDisplay)
return String(format: template, folderName)
case .unableToRenameFeed(let from, let to):
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly Could not rename a feed.")
return String(format: template, from, to)
case .unableToRemoveFeed(let feed):
case .unableToRemoveFeed(let feedName):
let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly Could not remove a feed.")
return String(format: template, feed.nameForDisplay)
return String(format: template, feedName)
}
}
@ -80,10 +80,10 @@ enum FeedlyAccountDelegateError: LocalizedError {
case .unableToRemoveFolder:
return nil
case .unableToMoveFeedBetweenFolders(let feed, let from, let to):
case .unableToMoveFeedBetweenFolders(let feedName, let sourceFolderName, let destinationFolderName):
let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly Could not move a feed between folders/collections.")
return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)
return String(format: template, feedName, sourceFolderName, destinationFolderName)
case .addFeedChooseFolder:
return nil

View File

@ -8,7 +8,7 @@
import Foundation
struct FeedlyFeedContainerValidator {
@MainActor struct FeedlyFeedContainerValidator {
var container: Container
func getValidContainer() throws -> (Folder, String) {
@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator {
}
guard let collectionId = folder.externalID else {
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay)
}
return (folder, collectionId)

View File

@ -23,7 +23,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error")
}
}
@objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding {
@MainActor @objc public final class OAuthAccountAuthorizationOperation: NSObject, MainThreadOperation, ASWebAuthenticationPresentationContextProviding {
public var isCanceled: Bool = false {
didSet {

View File

@ -10,7 +10,7 @@ import Foundation
import Articles
import Core
public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
@MainActor public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
public var containerID: ContainerIdentifier? {
guard let accountID = account?.accountID else {
@ -198,7 +198,7 @@ extension Folder: OPMLRepresentable {
extension Set where Element == Folder {
func sorted() -> Array<Folder> {
@MainActor func sorted() -> Array<Folder> {
return sorted(by: { (folder1, folder2) -> Bool in
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
})

View File

@ -52,7 +52,7 @@ final class LocalAccountRefresher {
extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
@MainActor func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
guard let feed = representedObject as? Feed else {
return nil
}
@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
return request
}
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
@MainActor func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
let feed = representedObject as! Feed
guard !data.isEmpty, !isSuspended else {
@ -101,18 +101,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
}
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) {
MainActor.assumeIsolated {
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)
}
} else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
}

View File

@ -328,45 +328,49 @@ extension NewsBlurAccountDelegate {
completion()
return
}
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) {
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
account.fetchUnreadArticleIDs { articleIDsResult in
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
MainActor.assumeIsolated {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
// Mark articles as unread
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
group.notify(queue: DispatchQueue.main) {
completion()
account.fetchUnreadArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as unread
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
}
}
}
}
@ -378,43 +382,47 @@ extension NewsBlurAccountDelegate {
}
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingStoryHashes: Set<String>) {
MainActor.assumeIsolated {
@MainActor func process(_ pendingStoryHashes: Set<String>) {
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
account.fetchStarredArticleIDs { articleIDsResult in
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as starred
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
account.fetchStarredArticleIDs { articleIDsResult in
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as starred
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
}
}
}
}

View File

@ -8,7 +8,7 @@
import Articles
import Database
import RSParser
@preconcurrency import RSParser
import RSWeb
import SyncDatabase
import os.log
@ -136,70 +136,73 @@ final class NewsBlurAccountDelegate: AccountDelegate {
database.selectForProcessing { result in
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
}
let deleteStarredStatuses = syncStatuses.filter {
$0.key == SyncStatus.Key.starred && $0.flag == false
}
let group = DispatchGroup()
var errorOccurred = false
group.enter()
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
group.leave()
if case .failure = result {
errorOccurred = true
MainActor.assumeIsolated {
@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
}
let deleteStarredStatuses = syncStatuses.filter {
$0.key == SyncStatus.Key.starred && $0.flag == false
}
let group = DispatchGroup()
var errorOccurred = false
group.enter()
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
if errorOccurred {
completion(.failure(NewsBlurError.unknown))
} else {
completion(.success(()))
}
}
}
group.enter()
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
group.enter()
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
if errorOccurred {
completion(.failure(NewsBlurError.unknown))
} else {
completion(.success(()))
}
}
}
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
}
}
@ -272,53 +275,55 @@ final class NewsBlurAccountDelegate: AccountDelegate {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
func process(_ fetchedHashes: Set<String>) {
let group = DispatchGroup()
var errorOccurred = false
MainActor.assumeIsolated {
@MainActor func process(_ fetchedHashes: Set<String>) {
let group = DispatchGroup()
var errorOccurred = false
let storyHashes = Array(fetchedHashes).map {
NewsBlurStoryHash(hash: $0, timestamp: Date())
}
let chunkedStoryHashes = storyHashes.chunked(into: 100)
let storyHashes = Array(fetchedHashes).map {
NewsBlurStoryHash(hash: $0, timestamp: Date())
}
let chunkedStoryHashes = storyHashes.chunked(into: 100)
for chunk in chunkedStoryHashes {
group.enter()
self.caller.retrieveStories(hashes: chunk) { result in
for chunk in chunkedStoryHashes {
group.enter()
self.caller.retrieveStories(hashes: chunk) { result in
switch result {
case .success((let stories, _)):
self.processStories(account: account, stories: stories) { result in
group.leave()
if case .failure = result {
errorOccurred = true
switch result {
case .success((let stories, _)):
self.processStories(account: account, stories: stories) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
case .failure(let error):
errorOccurred = true
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
group.leave()
}
case .failure(let error):
errorOccurred = true
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing missing stories.")
if errorOccurred {
completion(.failure(NewsBlurError.unknown))
} else {
completion(.success(()))
}
}
}
group.notify(queue: DispatchQueue.main) {
switch result {
case .success(let fetchedArticleIDs):
process(fetchedArticleIDs)
case .failure(let error):
self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing missing stories.")
if errorOccurred {
completion(.failure(NewsBlurError.unknown))
} else {
completion(.success(()))
}
completion(.failure(error))
}
}
switch result {
case .success(let fetchedArticleIDs):
process(fetchedArticleIDs)
case .failure(let error):
self.refreshProgress.completeTask()
completion(.failure(error))
}
}
}
@ -591,10 +596,12 @@ final class NewsBlurAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
}
completion(.success(()))
}
completion(.success(()))
}
}
case .failure(let error):

View File

@ -118,12 +118,14 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
switch result {
case .success(let articleIDs):
account.markAsRead(Set(articleIDs)) { _ in
self.refreshArticleStatus(for: account) { _ in
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
MainActor.assumeIsolated {
self.refreshArticleStatus(for: account) { _ in
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
}
}
}
}
@ -201,45 +203,48 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
database.selectForProcessing { result in
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 }
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup()
group.enter()
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) {
group.leave()
MainActor.assumeIsolated {
@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 }
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup()
group.enter()
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) {
group.leave()
}
group.enter()
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) {
group.leave()
}
group.enter()
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) {
group.leave()
}
group.enter()
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
completion(.success(()))
}
}
group.enter()
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) {
group.leave()
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
group.enter()
self.sendArticleStatuses(createStarredStatuses, apiCall: self.caller.createStarredEntries) {
group.leave()
}
group.enter()
self.sendArticleStatuses(deleteStarredStatuses, apiCall: self.caller.deleteStarredEntries) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
completion(.success(()))
}
}
switch result {
case .success(let syncStatuses):
processStatuses(syncStatuses)
case .failure(let databaseError):
completion(.failure(databaseError))
}
}
}
@ -617,10 +622,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
}
completion(.success(()))
}
completion(.success(()))
}
}
case .failure(let error):
@ -970,18 +977,19 @@ private extension ReaderAPIAccountDelegate {
switch result {
case .success(let articleIDs):
account.markAsRead(Set(articleIDs)) { _ in
self.refreshProgress.completeTask()
self.refreshArticleStatus(for: account) { _ in
MainActor.assumeIsolated {
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(feed))
self.refreshArticleStatus(for: account) { _ in
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(feed))
}
}
}
}
}
case .failure(let error):
completion(.failure(error))
@ -994,52 +1002,54 @@ private extension ReaderAPIAccountDelegate {
func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
func process(_ fetchedArticleIDs: Set<String>) {
guard !fetchedArticleIDs.isEmpty else {
completion()
return
}
os_log(.debug, log: self.log, "Refreshing missing articles...")
let group = DispatchGroup()
MainActor.assumeIsolated {
@MainActor func process(_ fetchedArticleIDs: Set<String>) {
guard !fetchedArticleIDs.isEmpty else {
completion()
return
}
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 150)
os_log(.debug, log: self.log, "Refreshing missing articles...")
let group = DispatchGroup()
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
let articleIDs = Array(fetchedArticleIDs)
let chunkedArticleIDs = articleIDs.chunked(into: 150)
for chunk in chunkedArticleIDs {
group.enter()
self.caller.retrieveEntries(articleIDs: chunk) { result in
self.refreshProgress.completeTask()
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
switch result {
case .success(let entries):
self.processEntries(account: account, entries: entries) {
for chunk in chunkedArticleIDs {
group.enter()
self.caller.retrieveEntries(articleIDs: chunk) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let entries):
self.processEntries(account: account, entries: entries) {
group.leave()
}
case .failure(let error):
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
group.leave()
}
case .failure(let error):
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing missing articles.")
completion()
}
}
group.notify(queue: DispatchQueue.main) {
switch articleIDsResult {
case .success(let articleIDs):
process(articleIDs)
case .failure:
self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing missing articles.")
completion()
}
}
switch articleIDsResult {
case .success(let articleIDs):
process(articleIDs)
case .failure:
self.refreshProgress.completeTask()
completion()
}
}
}
@ -1100,43 +1110,47 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchUnreadArticleIDs { articleIDsResult in
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
let group = DispatchGroup()
// Mark articles as unread
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
account.fetchUnreadArticleIDs { articleIDsResult in
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
MainActor.assumeIsolated {
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return
}
let group = DispatchGroup()
// Mark articles as unread
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
group.enter()
account.markAsUnread(deltaUnreadArticleIDs) { _ in
group.leave()
}
// Mark articles as read
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
group.enter()
account.markAsRead(deltaReadArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Read Status failed: %@.", error.localizedDescription)
}
}
}
@ -1149,43 +1163,47 @@ private extension ReaderAPIAccountDelegate {
database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
MainActor.assumeIsolated {
@MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableReaderUnreadArticleIDs = Set(articleIDs).subtracting(pendingArticleIDs)
account.fetchStarredArticleIDs { articleIDsResult in
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
account.fetchStarredArticleIDs { articleIDsResult in
let group = DispatchGroup()
// Mark articles as starred
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
return
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
let group = DispatchGroup()
group.notify(queue: DispatchQueue.main) {
completion()
// Mark articles as starred
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in
group.leave()
}
// Mark articles as unstarred
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
}
}
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
}
switch result {
case .success(let pendingArticleIDs):
process(pendingArticleIDs)
case .failure(let error):
os_log(.error, log: self.log, "Sync Article Starred Status failed: %@.", error.localizedDescription)
}
}
}
}

View File

@ -257,7 +257,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

View File

@ -17,12 +17,12 @@ extension Notification.Name {
public protocol DisplayNameProvider {
var nameForDisplay: String { get }
@MainActor var nameForDisplay: String { get }
}
public extension DisplayNameProvider {
func postDisplayNameDidChangeNotification() {
@MainActor func postDisplayNameDidChangeNotification() {
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil)
}

View File

@ -39,7 +39,7 @@ public protocol MainThreadOperation: AnyObject {
///
/// The completionBlock is always called on the main thread.
/// The queue will clear the completionBlock after calling it.
var completionBlock: MainThreadOperationCompletionBlock? { get set }
@MainActor var completionBlock: MainThreadOperationCompletionBlock? { get set }
/// Do the thing this operation does.
///

View File

@ -153,7 +153,7 @@ extension Feed: PasteboardWriterOwner {
}
}
@objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
@MainActor @objc final class FeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let feed: Feed
static let feedUTI = "com.ranchero.feed"

View File

@ -12,8 +12,8 @@ import AppKitExtras
typealias PasteboardFolderDictionary = [String: String]
struct PasteboardFolder: Hashable {
@MainActor struct PasteboardFolder: Hashable {
private struct Key {
static let name = "name"
// Internal
@ -91,7 +91,7 @@ extension Folder: PasteboardWriterOwner {
}
}
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
@MainActor @objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
private let folder: Folder
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"

View File

@ -12,7 +12,7 @@ import Articles
import Core
@objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
@MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let account:Account
init (_ account:Account) {

View File

@ -44,7 +44,7 @@ import Articles
// I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId)
var scriptingUniqueId:Any {
@MainActor var scriptingUniqueId:Any {
return feed.feedID
}
@ -71,7 +71,7 @@ import Articles
return url
}
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
@MainActor class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
let scriptableAccount = ScriptableAccount(account)
if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
@ -120,27 +120,27 @@ import Articles
// MARK: --- Scriptable properties ---
@objc(url)
var url:String {
@MainActor var url:String {
return self.feed.url
}
@objc(name)
var name:String {
@MainActor var name:String {
return self.feed.name ?? ""
}
@objc(homePageURL)
var homePageURL:String {
@MainActor var homePageURL:String {
return self.feed.homePageURL ?? ""
}
@objc(iconURL)
var iconURL:String {
@MainActor var iconURL:String {
return self.feed.iconURL ?? ""
}
@objc(faviconURL)
var faviconURL:String {
@MainActor var faviconURL:String {
return self.feed.faviconURL ?? ""
}
@ -152,13 +152,13 @@ import Articles
// MARK: --- scriptable elements ---
@objc(authors)
var authors:NSArray {
@MainActor var authors:NSArray {
let feedAuthors = feed.authors ?? []
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
}
@objc(valueInAuthorsWithUniqueID:)
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
@MainActor func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
return ScriptableAuthor(author, container:self)
}

View File

@ -12,7 +12,7 @@ import Articles
import Core
@objc(ScriptableFolder)
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
@MainActor class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let folder:Folder
let container:ScriptingObjectContainer

View File

@ -9,7 +9,7 @@
import Foundation
import Account
struct AddFeedDefaultContainer {
@MainActor struct AddFeedDefaultContainer {
@MainActor static var defaultContainer: Container? {

View File

@ -71,7 +71,7 @@ final class FaviconDownloader {
cache = [Feed: IconImage]()
}
func favicon(for feed: Feed) -> IconImage? {
@MainActor func favicon(for feed: Feed) -> IconImage? {
assert(Thread.isMainThread)
@ -93,7 +93,7 @@ final class FaviconDownloader {
return nil
}
func faviconAsIcon(for feed: Feed) -> IconImage? {
@MainActor func faviconAsIcon(for feed: Feed) -> IconImage? {
if let image = cache[feed] {
return image

View File

@ -37,7 +37,7 @@ struct ExtensionContainers: Codable {
}
struct ExtensionAccount: ExtensionContainer {
@MainActor struct ExtensionAccount: ExtensionContainer {
enum CodingKeys: String, CodingKey {
case name
@ -70,7 +70,7 @@ struct ExtensionAccount: ExtensionContainer {
}
struct ExtensionFolder: ExtensionContainer {
@MainActor struct ExtensionFolder: ExtensionContainer {
enum CodingKeys: String, CodingKey {
case accountName

View File

@ -9,7 +9,7 @@
import AppKit
import Account
@objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
@MainActor @objc final class SmartFeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let smartFeed: PseudoFeed

View File

@ -124,7 +124,7 @@ public extension SyncDatabase {
nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) {
Task {
Task { @MainActor in
do {
try await self.insertStatuses(statuses)
completion(nil)
@ -136,7 +136,7 @@ public extension SyncDatabase {
nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) {
Task {
Task { @MainActor in
do {
if let syncStatuses = try await self.selectForProcessing(limit: limit) {
completion(.success(Array(syncStatuses)))
@ -151,7 +151,7 @@ public extension SyncDatabase {
nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) {
Task {
Task { @MainActor in
do {
if let count = try await self.selectPendingCount() {
completion(.success(count))
@ -167,7 +167,7 @@ public extension SyncDatabase {
nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
Task {
Task { @MainActor in
do {
if let articleIDs = try await self.selectPendingReadStatusArticleIDs() {
completion(.success(articleIDs))
@ -182,7 +182,7 @@ public extension SyncDatabase {
nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) {
Task {
Task { @MainActor in
do {
if let articleIDs = try await self.selectPendingStarredStatusArticleIDs() {
completion(.success(articleIDs))
@ -197,7 +197,7 @@ public extension SyncDatabase {
nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) {
Task {
Task { @MainActor in
do {
try await self.resetAllSelectedForProcessing()
completion?(nil)
@ -209,7 +209,7 @@ public extension SyncDatabase {
nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) {
Task {
Task { @MainActor in
do {
try await self.resetSelectedForProcessing(articleIDs)
completion?(nil)