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>) 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 struct UserInfoKey {
public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount public static let account = "account" // UserDidAddAccount, UserDidDeleteAccount
@ -894,12 +894,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return return
} }
database.createStatusesIfNeeded(articleIDs: articleIDs) { error in database.createStatusesIfNeeded(articleIDs: articleIDs) { error in
if let error = error {
completion?(error) MainActor.assumeIsolated {
return 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 return
} }
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { error in
if let error { MainActor.assumeIsolated {
completion?(error) if let error {
} else { completion?(error)
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag) } else {
completion?(nil) self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
completion?(nil)
}
} }
} }
} }

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import Secrets
// Main thread only. // Main thread only.
public final class AccountManager: UnreadCountProvider { @MainActor public final class AccountManager: UnreadCountProvider {
@MainActor public static var shared: AccountManager! @MainActor public static var shared: AccountManager!
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml" 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") 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.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 { MainActor.assumeIsolated {
self.sendArticleStatus(for: account, showProgress: false) { _ in } if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account, showProgress: false) { _ in }
}
completion(.success(()))
} }
completion(.success(()))
} }
} }
case .failure(let error): case .failure(let error):
@ -648,35 +650,36 @@ private extension CloudKitAccountDelegate {
if let parsedFeed = parsedFeed { if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed) { result in account.update(feed, with: parsedFeed) { result in
switch result { MainActor.assumeIsolated {
case .success: switch result {
case .success:
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
name: parsedFeed.title, self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
editedName: editedName, name: parsedFeed.title,
homePageURL: parsedFeed.homePageURL, editedName: editedName,
container: container) { result in homePageURL: parsedFeed.homePageURL,
container: container) { result in
self.refreshProgress.completeTask()
switch result { self.refreshProgress.completeTask()
case .success(let externalID): switch result {
feed.externalID = externalID case .success(let externalID):
self.sendNewArticlesToTheCloud(account, feed) feed.externalID = externalID
completion(.success(feed)) self.sendNewArticlesToTheCloud(account, feed)
case .failure(let error): completion(.success(feed))
container.removeFeed(feed) case .failure(let error):
self.refreshProgress.completeTasks(2) container.removeFeed(feed)
completion(.failure(error)) 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 { } else {
self.refreshProgress.completeTasks(3) self.refreshProgress.completeTasks(3)
@ -700,9 +703,9 @@ private extension CloudKitAccountDelegate {
} }
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) { func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
Task { @MainActor in Task { @MainActor in
do { do {
let articles = try await account.articles(for: .feed(feed)) let articles = try await account.articles(for: .feed(feed))
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) { self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
@ -716,7 +719,7 @@ private extension CloudKitAccountDelegate {
} }
} }
} }
} catch { } catch {
os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) 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.") 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 var zoneID: CKRecordZone.ID

View File

@ -13,8 +13,8 @@ import CloudKit
import Articles import Articles
import CloudKitExtras import CloudKitExtras
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { @MainActor final class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String) private typealias UnclaimedFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, feedExternalID: String)
private var newUnclaimedFeeds = [String: [UnclaimedFeed]]() private var newUnclaimedFeeds = [String: [UnclaimedFeed]]()
private var existingUnclaimedFeeds = [String: [Feed]]() private var existingUnclaimedFeeds = [String: [Feed]]()
@ -135,7 +135,6 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
account?.removeFolder(folder) account?.removeFolder(folder)
} }
} }
} }
private extension CloudKitAcountZoneDelegate { 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 { guard let account = account, let articlesZone = articlesZone else {
completion(true) completion(true)
return return
@ -156,9 +156,11 @@ private extension CloudKitSendStatusOperation {
} }
case .failure(let error): case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
self.processAccountError(account, error) MainActor.assumeIsolated {
os_log(.error, log: self.log, "Send article status modify articles error: %@.", error.localizedDescription) self.processAccountError(account, error)
completion(true) 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 { if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelFeeds) account.removeFeeds(account.topLevelFeeds)
for folder in account.folders ?? Set<Folder>() { for folder in account.folders ?? Set<Folder>() {

View File

@ -15,20 +15,20 @@ extension Notification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
} }
public protocol Container: AnyObject, ContainerIdentifiable { @MainActor public protocol Container: AnyObject, ContainerIdentifiable {
var account: Account? { get } var account: Account? { get }
var topLevelFeeds: Set<Feed> { get set } var topLevelFeeds: Set<Feed> { get set }
var folders: Set<Folder>? { get set } var folders: Set<Folder>? { get set }
var externalID: String? { get set } var externalID: String? { get set }
func hasAtLeastOneFeed() -> Bool func hasAtLeastOneFeed() -> Bool
func objectIsChild(_ object: AnyObject) -> Bool func objectIsChild(_ object: AnyObject) -> Bool
func hasChildFolder(with: String) -> Bool func hasChildFolder(with: String) -> Bool
func childFolder(with: String) -> Folder? func childFolder(with: String) -> Folder?
func removeFeed(_ feed: Feed) func removeFeed(_ feed: Feed)
func addFeed(_ feed: Feed) func addFeed(_ feed: Feed)
//Recursive  checks subfolders //Recursive  checks subfolders

View File

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

View File

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

View File

@ -136,62 +136,64 @@ final class FeedbinAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { MainActor.assumeIsolated {
let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false } @MainActor func processStatuses(_ syncStatuses: [SyncStatus]) {
let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true } let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false } 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 let group = DispatchGroup()
var errorOccurred = false
group.enter()
self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in group.enter()
group.leave() self.sendArticleStatuses(createUnreadStatuses, apiCall: self.caller.createUnreadEntries) { result in
if case .failure = result { group.leave()
errorOccurred = true 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() switch result {
self.sendArticleStatuses(deleteUnreadStatuses, apiCall: self.caller.deleteUnreadEntries) { result in case .success(let syncStatuses):
group.leave() processStatuses(syncStatuses)
if case .failure = result { case .failure(let databaseError):
errorOccurred = true 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.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 { MainActor.assumeIsolated {
self.sendArticleStatus(for: account) { _ in } if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
}
completion(.success(()))
} }
completion(.success(()))
} }
} }
case .failure(let error): case .failure(let error):
@ -1082,43 +1086,46 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, page)): case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) { error in self.processEntries(account: account, entries: entries) { error in
MainActor.assumeIsolated {
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))
return return
} }
self.refreshArticleStatus(for: account) { result in self.refreshArticleStatus(for: account) { result in
switch result { switch result {
case .success: case .success:
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
switch result { switch result {
case .success: case .success:
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
self.refreshMissingArticles(account) { result in self.refreshMissingArticles(account) { result in
switch result { switch result {
case .success: case .success:
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.success(feed)) 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.processEntries(account: account, entries: entries) { error in
self.refreshProgress.completeTask()
if let error = error { MainActor.assumeIsolated {
completion(.failure(error))
return
}
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing articles.")
switch result { if let error = error {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(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)): case .success(let (entries, nextPage)):
self.processEntries(account: account, entries: entries) { error in self.processEntries(account: account, entries: entries) { error in
self.refreshProgress.completeTask() MainActor.assumeIsolated {
self.refreshProgress.completeTask()
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))
return return
}
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
} }
self.refreshArticles(account, page: nextPage, updateFetchDate: updateFetchDate, completion: completion)
} }
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -1294,47 +1306,50 @@ private extension FeedbinAccountDelegate {
database.selectPendingReadStatusArticleIDs() { result in database.selectPendingReadStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } ) @MainActor func process(_ pendingArticleIDs: Set<String>) {
let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
let feedbinUnreadArticleIDs = Set(articleIDs.map { String($0) } )
account.fetchUnreadArticleIDs { articleIDsResult in let updatableFeedbinUnreadArticleIDs = feedbinUnreadArticleIDs.subtracting(pendingArticleIDs)
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
return 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 database.selectPendingStarredStatusArticleIDs() { result in
func process(_ pendingArticleIDs: Set<String>) { MainActor.assumeIsolated {
@MainActor 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()
// Mark articles as starred let feedbinStarredArticleIDs = Set(articleIDs.map { String($0) } )
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs) let updatableFeedbinStarredArticleIDs = feedbinStarredArticleIDs.subtracting(pendingArticleIDs)
group.enter()
account.markAsStarred(deltaStarredArticleIDs) { _ in account.fetchStarredArticleIDs { articleIDsResult in
group.leave()
} MainActor.assumeIsolated {
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
// Mark articles as unstarred return
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableFeedbinStarredArticleIDs) }
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in let group = DispatchGroup()
group.leave()
} // Mark articles as starred
let deltaStarredArticleIDs = updatableFeedbinStarredArticleIDs.subtracting(currentStarredArticleIDs)
group.notify(queue: DispatchQueue.main) { group.enter()
completion() 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) { 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 { guard let folder = container as? Folder, let collectionId = folder.externalID else {
return DispatchQueue.main.async { 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(())) completion(.success(()))
case .failure: case .failure:
from.addFeed(feed) from.addFeed(feed)
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to))) completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
} }
} }
case .failure(let error): case .failure(let error):

View File

@ -14,12 +14,12 @@ enum FeedlyAccountDelegateError: LocalizedError {
case unableToAddFolder(String) case unableToAddFolder(String)
case unableToRenameFolder(String, String) case unableToRenameFolder(String, String)
case unableToRemoveFolder(String) case unableToRemoveFolder(String)
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder) case unableToMoveFeedBetweenFolders(String, String, String)
case addFeedChooseFolder case addFeedChooseFolder
case addFeedInvalidFolder(Folder) case addFeedInvalidFolder(String)
case unableToRenameFeed(String, String) case unableToRenameFeed(String, String)
case unableToRemoveFeed(Feed) case unableToRemoveFeed(String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .notLoggedIn: 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.") let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly Could not remove a folder/collection.")
return String(format: template, name) 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.") 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: case .addFeedChooseFolder:
return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly Feed can only be added to folders.") 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.") 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): case .unableToRenameFeed(let from, let to):
let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly Could not rename a feed.") let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly Could not rename a feed.")
return String(format: template, from, to) 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.") 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: case .unableToRemoveFolder:
return nil 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.") 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: case .addFeedChooseFolder:
return nil return nil

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct FeedlyFeedContainerValidator { @MainActor struct FeedlyFeedContainerValidator {
var container: Container var container: Container
func getValidContainer() throws -> (Folder, String) { func getValidContainer() throws -> (Folder, String) {
@ -17,7 +17,7 @@ struct FeedlyFeedContainerValidator {
} }
guard let collectionId = folder.externalID else { guard let collectionId = folder.externalID else {
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder.nameForDisplay)
} }
return (folder, collectionId) 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") 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 { public var isCanceled: Bool = false {
didSet { didSet {

View File

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

View File

@ -52,7 +52,7 @@ final class LocalAccountRefresher {
extension LocalAccountRefresher: DownloadSessionDelegate { 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 { guard let feed = representedObject as? Feed else {
return nil return nil
} }
@ -68,7 +68,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
return request 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 let feed = representedObject as! Feed
guard !data.isEmpty, !isSuspended else { guard !data.isEmpty, !isSuspended else {
@ -101,18 +101,20 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
} }
account.update(feed, with: parsedFeed) { result in account.update(feed, with: parsedFeed) { result in
if case .success(let articleChanges) = result { MainActor.assumeIsolated {
if let httpResponse = response as? HTTPURLResponse { if case .success(let articleChanges) = result {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse) if let httpResponse = response as? HTTPURLResponse {
} feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
feed.contentHash = dataHash }
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed) feed.contentHash = dataHash
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) { self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
completion()
}
} else {
completion() completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }
} else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
} }
} }

View File

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

View File

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

View File

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

View File

@ -17,12 +17,12 @@ extension Notification.Name {
public protocol DisplayNameProvider { public protocol DisplayNameProvider {
var nameForDisplay: String { get } @MainActor var nameForDisplay: String { get }
} }
public extension DisplayNameProvider { public extension DisplayNameProvider {
func postDisplayNameDidChangeNotification() { @MainActor func postDisplayNameDidChangeNotification() {
NotificationCenter.default.post(name: .DisplayNameDidChange, object: self, userInfo: nil) 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 completionBlock is always called on the main thread.
/// The queue will clear the completionBlock after calling it. /// 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. /// 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 private let feed: Feed
static let feedUTI = "com.ranchero.feed" static let feedUTI = "com.ranchero.feed"

View File

@ -12,8 +12,8 @@ import AppKitExtras
typealias PasteboardFolderDictionary = [String: String] typealias PasteboardFolderDictionary = [String: String]
struct PasteboardFolder: Hashable { @MainActor struct PasteboardFolder: Hashable {
private struct Key { private struct Key {
static let name = "name" static let name = "name"
// Internal // 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 private let folder: Folder
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder" static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"

View File

@ -12,7 +12,7 @@ import Articles
import Core import Core
@objc(ScriptableAccount) @objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { @MainActor class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let account:Account let account:Account
init (_ 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 // 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 // but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { @MainActor var scriptingUniqueId:Any {
return feed.feedID return feed.feedID
} }
@ -71,7 +71,7 @@ import Articles
return url 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) let scriptableAccount = ScriptableAccount(account)
if let folder = folder { if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
@ -120,27 +120,27 @@ import Articles
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(url) @objc(url)
var url:String { @MainActor var url:String {
return self.feed.url return self.feed.url
} }
@objc(name) @objc(name)
var name:String { @MainActor var name:String {
return self.feed.name ?? "" return self.feed.name ?? ""
} }
@objc(homePageURL) @objc(homePageURL)
var homePageURL:String { @MainActor var homePageURL:String {
return self.feed.homePageURL ?? "" return self.feed.homePageURL ?? ""
} }
@objc(iconURL) @objc(iconURL)
var iconURL:String { @MainActor var iconURL:String {
return self.feed.iconURL ?? "" return self.feed.iconURL ?? ""
} }
@objc(faviconURL) @objc(faviconURL)
var faviconURL:String { @MainActor var faviconURL:String {
return self.feed.faviconURL ?? "" return self.feed.faviconURL ?? ""
} }
@ -152,13 +152,13 @@ import Articles
// MARK: --- scriptable elements --- // MARK: --- scriptable elements ---
@objc(authors) @objc(authors)
var authors:NSArray { @MainActor var authors:NSArray {
let feedAuthors = feed.authors ?? [] let feedAuthors = feed.authors ?? []
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
} }
@objc(valueInAuthorsWithUniqueID:) @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 } guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
return ScriptableAuthor(author, container:self) return ScriptableAuthor(author, container:self)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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