Change LocalAccountRefresher to use a delegate so that it can better handle duplicate feed requests
This commit is contained in:
parent
78694aa07b
commit
63c4d53ee9
|
@ -34,7 +34,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
private let accountZone: CloudKitAccountZone
|
private let accountZone: CloudKitAccountZone
|
||||||
private let articlesZone: CloudKitArticlesZone
|
private let articlesZone: CloudKitArticlesZone
|
||||||
|
|
||||||
private let refresher = LocalAccountRefresher()
|
weak var account: Account?
|
||||||
|
|
||||||
|
private lazy var refresher: LocalAccountRefresher? = {
|
||||||
|
let refresher = LocalAccountRefresher()
|
||||||
|
refresher.delegate = self
|
||||||
|
return refresher
|
||||||
|
}()
|
||||||
|
|
||||||
let behaviors: AccountBehaviors = []
|
let behaviors: AccountBehaviors = []
|
||||||
let isOPMLImportInProgress = false
|
let isOPMLImportInProgress = false
|
||||||
|
@ -44,7 +50,8 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
var accountMetadata: AccountMetadata?
|
var accountMetadata: AccountMetadata?
|
||||||
|
|
||||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||||
|
var refreshAllCompletion: ((Result<Void, Error>) -> Void)? = nil
|
||||||
|
|
||||||
init(dataFolder: String) {
|
init(dataFolder: String) {
|
||||||
accountZone = CloudKitAccountZone(container: container)
|
accountZone = CloudKitAccountZone(container: container)
|
||||||
articlesZone = CloudKitArticlesZone(container: container)
|
articlesZone = CloudKitArticlesZone(container: container)
|
||||||
|
@ -72,11 +79,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard refreshProgress.isComplete else {
|
guard refreshAllCompletion == nil else {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refreshAll(for: account, downloadFeeds: true, completion: completion)
|
refreshAllCompletion = completion
|
||||||
|
|
||||||
|
refreshAll(for: account, downloadFeeds: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
|
@ -146,6 +155,12 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
guard refreshAllCompletion == nil else {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshAllCompletion = completion
|
||||||
|
|
||||||
var fileData: Data?
|
var fileData: Data?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -199,7 +214,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
||||||
self.refreshAll(for: account, downloadFeeds: false, completion: completion)
|
self.refreshAll(for: account, downloadFeeds: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -463,6 +478,8 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountDidInitialize(_ account: Account) {
|
func accountDidInitialize(_ account: Account) {
|
||||||
|
self.account = account
|
||||||
|
|
||||||
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
|
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
|
||||||
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone)
|
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone)
|
||||||
|
|
||||||
|
@ -472,11 +489,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let externalID):
|
case .success(let externalID):
|
||||||
account.externalID = externalID
|
account.externalID = externalID
|
||||||
self.refreshAll(for: account, downloadFeeds: false) { result in
|
self.refreshAll(for: account, downloadFeeds: false)
|
||||||
if case .failure(let error) = result {
|
|
||||||
os_log(.error, log: self.log, "Error while doing intial refresh: %@", error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
@ -501,7 +514,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
// MARK: Suspend and Resume (for iOS)
|
// MARK: Suspend and Resume (for iOS)
|
||||||
|
|
||||||
func suspendNetwork() {
|
func suspendNetwork() {
|
||||||
refresher.suspend()
|
refresher?.suspend()
|
||||||
}
|
}
|
||||||
|
|
||||||
func suspendDatabase() {
|
func suspendDatabase() {
|
||||||
|
@ -509,18 +522,25 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
refresher.resume()
|
refresher?.resume()
|
||||||
database.resume()
|
database.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CloudKitAccountDelegate {
|
private extension CloudKitAccountDelegate {
|
||||||
|
|
||||||
func refreshAll(for account: Account, downloadFeeds: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
|
func refreshAll(for account: Account, downloadFeeds: Bool) {
|
||||||
|
|
||||||
let intialWebFeedsCount = downloadFeeds ? account.flattenedWebFeeds().count : 0
|
let intialWebFeedsCount = downloadFeeds ? account.flattenedWebFeeds().count : 0
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
|
refreshProgress.addToNumberOfTasksAndRemaining(3 + intialWebFeedsCount)
|
||||||
|
|
||||||
|
func fail(_ error: Error) {
|
||||||
|
self.processAccountError(account, error)
|
||||||
|
self.refreshProgress.clear()
|
||||||
|
self.refreshAllCompletion?(.failure(error))
|
||||||
|
self.refreshAllCompletion = nil
|
||||||
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
accountZone.fetchChangesInZone() { result in
|
accountZone.fetchChangesInZone() { result in
|
||||||
BatchUpdate.shared.end()
|
BatchUpdate.shared.end()
|
||||||
|
@ -545,35 +565,26 @@ private extension CloudKitAccountDelegate {
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
guard downloadFeeds else {
|
guard downloadFeeds else {
|
||||||
completion(.success(()))
|
self.refreshAllCompletion?(.success(()))
|
||||||
|
self.refreshAllCompletion = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresher.refreshFeeds(webFeeds, feedCompletionBlock: { _ in self.refreshProgress.completeTask() }) {
|
self.refresher?.refreshFeeds(webFeeds)
|
||||||
account.metadata.lastArticleFetchEndTime = Date()
|
|
||||||
self.refreshProgress.clear()
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
self.processAccountError(account, error)
|
fail(error)
|
||||||
self.refreshProgress.clear()
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
self.processAccountError(account, error)
|
fail(error)
|
||||||
self.refreshProgress.clear()
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
self.processAccountError(account, error)
|
fail(error)
|
||||||
self.refreshProgress.clear()
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -588,3 +599,18 @@ private extension CloudKitAccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
|
||||||
|
|
||||||
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
||||||
|
refreshProgress.completeTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
|
||||||
|
self.refreshProgress.clear()
|
||||||
|
account?.metadata.lastArticleFetchEndTime = Date()
|
||||||
|
refreshAllCompletion?(.success(()))
|
||||||
|
refreshAllCompletion = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,13 @@ public enum LocalAccountDelegateError: String, Error {
|
||||||
|
|
||||||
final class LocalAccountDelegate: AccountDelegate {
|
final class LocalAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
private let refresher = LocalAccountRefresher()
|
weak var account: Account?
|
||||||
|
|
||||||
|
private lazy var refresher: LocalAccountRefresher? = {
|
||||||
|
let refresher = LocalAccountRefresher()
|
||||||
|
refresher.delegate = self
|
||||||
|
return refresher
|
||||||
|
}()
|
||||||
|
|
||||||
let behaviors: AccountBehaviors = []
|
let behaviors: AccountBehaviors = []
|
||||||
let isOPMLImportInProgress = false
|
let isOPMLImportInProgress = false
|
||||||
|
@ -28,19 +34,23 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||||
var accountMetadata: AccountMetadata?
|
var accountMetadata: AccountMetadata?
|
||||||
|
|
||||||
let refreshProgress = DownloadProgress(numberOfTasks: 0)
|
let refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||||
|
var refreshAllCompletion: ((Result<Void, Error>) -> Void)? = nil
|
||||||
|
|
||||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
guard refreshAllCompletion == nil else {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAllCompletion = completion
|
||||||
|
|
||||||
let webFeeds = account.flattenedWebFeeds()
|
let webFeeds = account.flattenedWebFeeds()
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
|
refreshProgress.addToNumberOfTasksAndRemaining(webFeeds.count)
|
||||||
refresher.refreshFeeds(webFeeds, feedCompletionBlock: { _ in self.refreshProgress.completeTask() }) {
|
refresher?.refreshFeeds(webFeeds)
|
||||||
self.refreshProgress.clear()
|
|
||||||
account.metadata.lastArticleFetchEndTime = Date()
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
|
@ -196,6 +206,7 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountDidInitialize(_ account: Account) {
|
func accountDidInitialize(_ account: Account) {
|
||||||
|
self.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountWillBeDeleted(_ account: Account) {
|
func accountWillBeDeleted(_ account: Account) {
|
||||||
|
@ -208,7 +219,7 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||||
// MARK: Suspend and Resume (for iOS)
|
// MARK: Suspend and Resume (for iOS)
|
||||||
|
|
||||||
func suspendNetwork() {
|
func suspendNetwork() {
|
||||||
refresher.suspend()
|
refresher?.suspend()
|
||||||
}
|
}
|
||||||
|
|
||||||
func suspendDatabase() {
|
func suspendDatabase() {
|
||||||
|
@ -216,6 +227,21 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
refresher.resume()
|
refresher?.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
||||||
|
|
||||||
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
||||||
|
refreshProgress.completeTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher) {
|
||||||
|
self.refreshProgress.clear()
|
||||||
|
account?.metadata.lastArticleFetchEndTime = Date()
|
||||||
|
refreshAllCompletion?(.success(()))
|
||||||
|
refreshAllCompletion = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,19 +12,21 @@ import RSParser
|
||||||
import RSWeb
|
import RSWeb
|
||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
|
protocol LocalAccountRefresherDelegate {
|
||||||
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed)
|
||||||
|
func localAccountRefresherDidFinish(_ refresher: LocalAccountRefresher)
|
||||||
|
}
|
||||||
|
|
||||||
final class LocalAccountRefresher {
|
final class LocalAccountRefresher {
|
||||||
|
|
||||||
private var feedCompletionBlock: ((WebFeed) -> Void)?
|
|
||||||
private var completion: (() -> Void)?
|
|
||||||
private var isSuspended = false
|
private var isSuspended = false
|
||||||
|
var delegate: LocalAccountRefresherDelegate?
|
||||||
|
|
||||||
private lazy var downloadSession: DownloadSession = {
|
private lazy var downloadSession: DownloadSession = {
|
||||||
return DownloadSession(delegate: self)
|
return DownloadSession(delegate: self)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public func refreshFeeds(_ feeds: Set<WebFeed>, feedCompletionBlock: @escaping (WebFeed) -> Void, completion: @escaping () -> Void) {
|
public func refreshFeeds(_ feeds: Set<WebFeed>) {
|
||||||
self.feedCompletionBlock = feedCompletionBlock
|
|
||||||
self.completion = completion
|
|
||||||
downloadSession.downloadObjects(feeds as NSSet)
|
downloadSession.downloadObjects(feeds as NSSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,21 +66,21 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
|
|
||||||
guard !data.isEmpty, !isSuspended else {
|
guard !data.isEmpty, !isSuspended else {
|
||||||
completion()
|
completion()
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Error downloading \(feed.url) - \(error)")
|
print("Error downloading \(feed.url) - \(error)")
|
||||||
completion()
|
completion()
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataHash = data.md5String
|
let dataHash = data.md5String
|
||||||
if dataHash == feed.contentHash {
|
if dataHash == feed.contentHash {
|
||||||
completion()
|
completion()
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
|
|
||||||
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
|
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
|
||||||
completion()
|
completion()
|
||||||
self.feedCompletionBlock?(feed)
|
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +102,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
feed.contentHash = dataHash
|
feed.contentHash = dataHash
|
||||||
}
|
}
|
||||||
completion()
|
completion()
|
||||||
self.feedCompletionBlock?(feed)
|
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -109,7 +111,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
|
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! WebFeed
|
||||||
guard !isSuspended else {
|
guard !isSuspended else {
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.isDefinitelyNotFeed() {
|
if data.isDefinitelyNotFeed() {
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +129,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
if FeedParser.mightBeAbleToParseBasedOnPartialData(parserData) {
|
if FeedParser.mightBeAbleToParseBasedOnPartialData(parserData) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,17 +139,21 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
|
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! WebFeed
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
|
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! WebFeed
|
||||||
feedCompletionBlock?(feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
|
||||||
|
let feed = representedObject as! WebFeed
|
||||||
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
|
}
|
||||||
|
|
||||||
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
|
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
|
||||||
completion?()
|
delegate?.localAccountRefresherDidFinish(self)
|
||||||
completion = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 88d634f5fd42aab203b6e53c7b551a92b03ffc97
|
Subproject commit f54e1cbad3917822e40bc2310ed24bd7cb688a4f
|
Loading…
Reference in New Issue