Use MainThreadOperation to prevent article status updates from interleaving
This commit is contained in:
parent
6bdcd941ae
commit
85f7adaa0a
@ -89,6 +89,9 @@
|
|||||||
51E4DB302426353D0091EB5B /* CloudKitAccountZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */; };
|
51E4DB302426353D0091EB5B /* CloudKitAccountZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */; };
|
||||||
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; };
|
51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; };
|
||||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; };
|
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; };
|
||||||
|
51F6C58F245DB2E8001E41CA /* CloudKitSendStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */; };
|
||||||
|
51F6C591245DB302001E41CA /* CloudKitReceiveStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */; };
|
||||||
|
51F6C593245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */; };
|
||||||
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; };
|
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; };
|
||||||
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; };
|
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; };
|
||||||
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; };
|
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; };
|
||||||
@ -340,6 +343,9 @@
|
|||||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZone.swift; sourceTree = "<group>"; };
|
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZone.swift; sourceTree = "<group>"; };
|
||||||
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
|
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
|
||||||
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
|
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
|
||||||
|
51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitSendStatusOperation.swift; sourceTree = "<group>"; };
|
||||||
|
51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitReceiveStatusOperation.swift; sourceTree = "<group>"; };
|
||||||
|
51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRemoteNotificationOperation.swift; sourceTree = "<group>"; };
|
||||||
552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = "<group>"; };
|
552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = "<group>"; };
|
||||||
552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = "<group>"; };
|
552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = "<group>"; };
|
||||||
552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = "<group>"; };
|
552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = "<group>"; };
|
||||||
@ -555,10 +561,13 @@
|
|||||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
||||||
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */,
|
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */,
|
||||||
|
5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */,
|
||||||
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */,
|
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */,
|
||||||
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */,
|
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */,
|
||||||
5150FFFD243823B800C1A442 /* CloudKitError.swift */,
|
5150FFFD243823B800C1A442 /* CloudKitError.swift */,
|
||||||
5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */,
|
51F6C590245DB302001E41CA /* CloudKitReceiveStatusOperation.swift */,
|
||||||
|
51F6C592245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift */,
|
||||||
|
51F6C58E245DB2E8001E41CA /* CloudKitSendStatusOperation.swift */,
|
||||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||||
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
|
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
|
||||||
);
|
);
|
||||||
@ -1137,6 +1146,7 @@
|
|||||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */,
|
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */,
|
||||||
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */,
|
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */,
|
||||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */,
|
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */,
|
||||||
|
51F6C591245DB302001E41CA /* CloudKitReceiveStatusOperation.swift in Sources */,
|
||||||
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */,
|
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */,
|
||||||
514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */,
|
514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */,
|
||||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */,
|
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */,
|
||||||
@ -1176,6 +1186,7 @@
|
|||||||
519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */,
|
519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */,
|
||||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
|
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
|
||||||
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */,
|
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */,
|
||||||
|
51F6C593245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift in Sources */,
|
||||||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
||||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
||||||
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
|
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
|
||||||
@ -1280,6 +1291,7 @@
|
|||||||
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */,
|
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */,
|
||||||
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */,
|
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */,
|
||||||
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */,
|
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */,
|
||||||
|
51F6C58F245DB2E8001E41CA /* CloudKitSendStatusOperation.swift in Sources */,
|
||||||
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */,
|
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -18,9 +18,15 @@ import ArticlesDatabase
|
|||||||
import RSWeb
|
import RSWeb
|
||||||
import Secrets
|
import Secrets
|
||||||
|
|
||||||
public enum CloudKitAccountDelegateError: String, Error {
|
enum CloudKitAccountDelegateError: LocalizedError {
|
||||||
case invalidParameter = "An invalid parameter was used."
|
case invalidParameter
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
final class CloudKitAccountDelegate: AccountDelegate {
|
final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
@ -33,10 +39,11 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire")
|
return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var zones: [CloudKitZone] = [accountZone, articlesZone]
|
|
||||||
private let accountZone: CloudKitAccountZone
|
private let accountZone: CloudKitAccountZone
|
||||||
private let articlesZone: CloudKitArticlesZone
|
private let articlesZone: CloudKitArticlesZone
|
||||||
|
|
||||||
|
private let mainThreadOperationQueue = MainThreadOperationQueue()
|
||||||
|
|
||||||
private lazy var refresher: LocalAccountRefresher = {
|
private lazy var refresher: LocalAccountRefresher = {
|
||||||
let refresher = LocalAccountRefresher()
|
let refresher = LocalAccountRefresher()
|
||||||
refresher.delegate = self
|
refresher.delegate = self
|
||||||
@ -63,21 +70,11 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||||
os_log(.debug, log: log, "Processing remote notification...")
|
let op = CloudKitRemoteNotificationOperation(accountZone: accountZone, articlesZone: articlesZone, userInfo: userInfo)
|
||||||
|
op.completionBlock = { mainThreadOperaion in
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
zones.forEach { zone in
|
|
||||||
group.enter()
|
|
||||||
zone.receiveRemoteNotification(userInfo: userInfo) {
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: DispatchQueue.main) {
|
|
||||||
os_log(.debug, log: self.log, "Done processing remote notification...")
|
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
mainThreadOperationQueue.add(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
@ -101,17 +98,15 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
os_log(.debug, log: log, "Refreshing article statuses...")
|
let op = CloudKitReceiveStatusOperation(articlesZone: articlesZone)
|
||||||
|
op.completionBlock = { mainThreadOperaion in
|
||||||
articlesZone.refreshArticles() { result in
|
if mainThreadOperaion.isCanceled {
|
||||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
completion(.failure(CloudKitAccountDelegateError.unknown))
|
||||||
switch result {
|
} else {
|
||||||
case .success:
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mainThreadOperationQueue.add(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
@ -420,17 +415,15 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zones.forEach { zone in
|
accountZone.subscribeToZoneChanges()
|
||||||
zone.subscribeToZoneChanges()
|
articlesZone.subscribeToZoneChanges()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountWillBeDeleted(_ account: Account) {
|
func accountWillBeDeleted(_ account: Account) {
|
||||||
zones.forEach { zone in
|
accountZone.resetChangeToken()
|
||||||
zone.resetChangeToken()
|
articlesZone.resetChangeToken()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
|
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
|
||||||
@ -770,94 +763,19 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func sendArticleStatus(for account: Account, showProgress: Bool, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
os_log(.debug, log: log, "Sending article statuses...")
|
let op = CloudKitSendStatusOperation(account: account,
|
||||||
|
articlesZone: articlesZone,
|
||||||
database.selectForProcessing { result in
|
refreshProgress: refreshProgress,
|
||||||
|
showProgress: showProgress,
|
||||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
database: database)
|
||||||
guard syncStatuses.count > 0 else {
|
op.completionBlock = { mainThreadOperaion in
|
||||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
if mainThreadOperaion.isCanceled {
|
||||||
completion(.success(()))
|
completion(.failure(CloudKitAccountDelegateError.unknown))
|
||||||
return
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
let group = DispatchGroup()
|
|
||||||
let syncStatusChunks = syncStatuses.chunked(into: 300)
|
|
||||||
|
|
||||||
if showProgress {
|
|
||||||
self.refreshProgress.addToNumberOfTasksAndRemaining(syncStatusChunks.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
for syncStatusChunk in syncStatusChunks {
|
|
||||||
group.enter()
|
|
||||||
self.sendArticleStatusChunk(for: account, syncStatuses: syncStatusChunk, showProgress: showProgress) {
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: DispatchQueue.main) {
|
|
||||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let syncStatuses):
|
|
||||||
processStatuses(syncStatuses)
|
|
||||||
case .failure(let databaseError):
|
|
||||||
completion(.failure(databaseError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendArticleStatusChunk(for account: Account, syncStatuses: [SyncStatus], showProgress: Bool, completion: @escaping () -> Void) {
|
|
||||||
|
|
||||||
let articleIDs = syncStatuses.map({ $0.articleID })
|
|
||||||
account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in
|
|
||||||
|
|
||||||
func processWithArticles(_ articles: Set<Article>) {
|
|
||||||
|
|
||||||
let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID })
|
|
||||||
let articlesDict = articles.reduce(into: [String: Article]()) { result, article in
|
|
||||||
result[article.articleID] = article
|
|
||||||
}
|
|
||||||
let statusUpdates = syncStatusesDict.map { (key, value) in
|
|
||||||
return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
self.articlesZone.modifyArticles(statusUpdates) { result in
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
|
||||||
if showProgress {
|
|
||||||
self.refreshProgress.completeTask()
|
|
||||||
}
|
|
||||||
os_log(.debug, log: self.log, "Done sending article status block...")
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let articles):
|
|
||||||
processWithArticles(articles)
|
|
||||||
case .failure(let databaseError):
|
|
||||||
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
|
||||||
os_log(.error, log: self.log, "Send article status fetch articles error: %@.", databaseError.localizedDescription)
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
mainThreadOperationQueue.add(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -875,3 +793,4 @@ extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// CloudKitReceiveStatusOperation.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 5/2/20.
|
||||||
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import os.log
|
||||||
|
import RSCore
|
||||||
|
|
||||||
|
class CloudKitReceiveStatusOperation: MainThreadOperation {
|
||||||
|
|
||||||
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
|
// MainThreadOperation
|
||||||
|
public var isCanceled = false
|
||||||
|
public var id: Int?
|
||||||
|
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||||
|
public var name: String? = "CloudKitReceiveStatusOperation"
|
||||||
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
|
private weak var articlesZone: CloudKitArticlesZone?
|
||||||
|
|
||||||
|
init(articlesZone: CloudKitArticlesZone) {
|
||||||
|
self.articlesZone = articlesZone
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
guard let articlesZone = articlesZone else {
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os_log(.debug, log: log, "Refreshing article statuses...")
|
||||||
|
|
||||||
|
articlesZone.refreshArticles() { result in
|
||||||
|
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.error, log: self.log, "Receive status error: %@.", error.localizedDescription)
|
||||||
|
self.operationDelegate?.cancelOperation(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// CloudKitRemoteNotificationOperation.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 5/2/20.
|
||||||
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import RSCore
|
||||||
|
|
||||||
|
class CloudKitRemoteNotificationOperation: MainThreadOperation {
|
||||||
|
|
||||||
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
|
// MainThreadOperation
|
||||||
|
public var isCanceled = false
|
||||||
|
public var id: Int?
|
||||||
|
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||||
|
public var name: String? = "CloudKitRemoteNotificationOperation"
|
||||||
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
|
private weak var accountZone: CloudKitAccountZone?
|
||||||
|
private weak var articlesZone: CloudKitArticlesZone?
|
||||||
|
private var userInfo: [AnyHashable : Any]
|
||||||
|
|
||||||
|
init(accountZone: CloudKitAccountZone, articlesZone: CloudKitArticlesZone, userInfo: [AnyHashable : Any]) {
|
||||||
|
self.accountZone = accountZone
|
||||||
|
self.articlesZone = articlesZone
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
guard let accountZone = accountZone, let articlesZone = articlesZone else {
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os_log(.debug, log: log, "Processing remote notification...")
|
||||||
|
|
||||||
|
accountZone.receiveRemoteNotification(userInfo: userInfo) {
|
||||||
|
articlesZone.receiveRemoteNotification(userInfo: self.userInfo) {
|
||||||
|
os_log(.debug, log: self.log, "Done processing remote notification.")
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
147
Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift
Normal file
147
Frameworks/Account/CloudKit/CloudKitSendStatusOperation.swift
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// CloudKitSendStatusOperation.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 5/2/20.
|
||||||
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Articles
|
||||||
|
import os.log
|
||||||
|
import RSCore
|
||||||
|
import RSWeb
|
||||||
|
import SyncDatabase
|
||||||
|
|
||||||
|
class CloudKitSendStatusOperation: MainThreadOperation {
|
||||||
|
|
||||||
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
|
// MainThreadOperation
|
||||||
|
public var isCanceled = false
|
||||||
|
public var id: Int?
|
||||||
|
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||||
|
public var name: String? = "CloudKitSendStatusOperation"
|
||||||
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
|
private weak var account: Account?
|
||||||
|
private weak var articlesZone: CloudKitArticlesZone?
|
||||||
|
private weak var refreshProgress: DownloadProgress?
|
||||||
|
private var showProgress: Bool
|
||||||
|
private var database: SyncDatabase
|
||||||
|
|
||||||
|
init(account: Account, articlesZone: CloudKitArticlesZone, refreshProgress: DownloadProgress, showProgress: Bool, database: SyncDatabase) {
|
||||||
|
self.account = account
|
||||||
|
self.articlesZone = articlesZone
|
||||||
|
self.refreshProgress = refreshProgress
|
||||||
|
self.showProgress = showProgress
|
||||||
|
self.database = database
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
os_log(.debug, log: log, "Sending article statuses...")
|
||||||
|
|
||||||
|
database.selectForProcessing { result in
|
||||||
|
|
||||||
|
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||||
|
guard syncStatuses.count > 0 else {
|
||||||
|
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
let syncStatusChunks = syncStatuses.chunked(into: 300)
|
||||||
|
|
||||||
|
if self.showProgress {
|
||||||
|
self.refreshProgress?.addToNumberOfTasksAndRemaining(syncStatusChunks.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
for syncStatusChunk in syncStatusChunks {
|
||||||
|
group.enter()
|
||||||
|
self.sendArticleStatusChunk(syncStatuses: syncStatusChunk) {
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||||
|
self.operationDelegate?.operationDidComplete(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let syncStatuses):
|
||||||
|
processStatuses(syncStatuses)
|
||||||
|
case .failure(let databaseError):
|
||||||
|
os_log(.error, log: self.log, "Send status error: %@.", databaseError.localizedDescription)
|
||||||
|
self.operationDelegate?.cancelOperation(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendArticleStatusChunk(syncStatuses: [SyncStatus], completion: @escaping () -> Void) {
|
||||||
|
guard let account = account, let articlesZone = articlesZone else {
|
||||||
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let articleIDs = syncStatuses.map({ $0.articleID })
|
||||||
|
account.fetchArticlesAsync(.articleIDs(Set(articleIDs))) { result in
|
||||||
|
|
||||||
|
func processWithArticles(_ articles: Set<Article>) {
|
||||||
|
|
||||||
|
let syncStatusesDict = Dictionary(grouping: syncStatuses, by: { $0.articleID })
|
||||||
|
let articlesDict = articles.reduce(into: [String: Article]()) { result, article in
|
||||||
|
result[article.articleID] = article
|
||||||
|
}
|
||||||
|
let statusUpdates = syncStatusesDict.map { (key, value) in
|
||||||
|
return CloudKitArticleStatusUpdate(articleID: key, statuses: value, article: articlesDict[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
articlesZone.modifyArticles(statusUpdates) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||||
|
if self.showProgress {
|
||||||
|
self.refreshProgress?.completeTask()
|
||||||
|
}
|
||||||
|
os_log(.debug, log: self.log, "Done sending article status block...")
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let articles):
|
||||||
|
processWithArticles(articles)
|
||||||
|
case .failure(let databaseError):
|
||||||
|
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID })) { _ in
|
||||||
|
os_log(.error, log: self.log, "Send article status fetch articles error: %@.", databaseError.localizedDescription)
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAccountError(_ account: Account, _ error: Error) {
|
||||||
|
if case CloudKitZoneError.userDeletedZone = error {
|
||||||
|
account.removeFeeds(account.topLevelWebFeeds)
|
||||||
|
for folder in account.folders ?? Set<Folder>() {
|
||||||
|
account.removeFolder(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,7 +24,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||||
|
|
||||||
var syncTimer: ArticleStatusSyncTimer?
|
var syncTimer: ArticleStatusSyncTimer?
|
||||||
private let remoteNotificationOperationQueue = MainThreadOperationQueue()
|
|
||||||
|
|
||||||
var shuttingDown = false {
|
var shuttingDown = false {
|
||||||
didSet {
|
didSet {
|
||||||
@ -112,9 +111,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completion: @escaping (UIBackgroundFetchResult) -> Void) {
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||||
let op = RemoteNotificationOperation(userInfo: userInfo, completion: completion)
|
DispatchQueue.main.async {
|
||||||
remoteNotificationOperationQueue.add(op)
|
self.resumeDatabaseProcessingIfNecessary()
|
||||||
|
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
||||||
|
self.suspendApplication()
|
||||||
|
completionHandler(.newData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ application: UIApplication) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
@ -391,31 +395,3 @@ private extension AppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteNotificationOperation: MainThreadOperation {
|
|
||||||
|
|
||||||
// MainThreadOperation
|
|
||||||
public var isCanceled = false
|
|
||||||
public var id: Int?
|
|
||||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
|
||||||
public var name: String? = "RemoteNotificationOperation"
|
|
||||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
|
||||||
|
|
||||||
private var userInfo: [AnyHashable : Any]
|
|
||||||
private var completion: (UIBackgroundFetchResult) -> Void
|
|
||||||
|
|
||||||
init(userInfo: [AnyHashable : Any], completion: @escaping (UIBackgroundFetchResult) -> Void) {
|
|
||||||
self.userInfo = userInfo
|
|
||||||
self.completion = completion
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() {
|
|
||||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
|
||||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
|
||||||
appDelegate.suspendApplication()
|
|
||||||
self.completion(.newData)
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user