Move LocalAccountRefresher from the Account module to the LocalAccount module. Make it use feedURL (String) rather than Feed objects, since it no longer knows about Feed objects. Update LocalAccountRefresherDelegate to match.

This commit is contained in:
Brent Simmons
2023-09-04 14:17:39 -07:00
parent df832f4b41
commit 72dec6091c
5 changed files with 279 additions and 200 deletions

View File

@@ -51,12 +51,13 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
}
let feeds = account.flattenedFeeds()
refreshProgress.addToNumberOfTasksAndRemaining(feeds.count)
let feedURLs = Set(feeds.map{ $0.url })
refreshProgress.addToNumberOfTasksAndRemaining(feedURLs.count)
let group = DispatchGroup()
group.enter()
refresher?.refreshFeeds(feeds) {
refresher?.refreshFeedURLs(feedURLs) {
group.leave()
}
@@ -224,16 +225,45 @@ final class LocalAccountDelegate: AccountDelegate, Logging {
}
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
refreshProgress.completeTask()
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void) {
completion()
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestForFeedURL feedURL: String) -> URLRequest? {
guard let url = URL(string: feedURL) else {
return nil
}
var request = URLRequest(url: url)
if let feed = account?.existingFeed(withURL: feedURL) {
feed.conditionalGetInfo?.addRequestHeadersToURLRequest(&request)
}
return request
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, feedURL: String, response: URLResponse?, data: Data, error: Error?, completion: @escaping () -> Void) {
guard !data.isEmpty else {
completion()
return
}
if let error = error {
print("Error downloading \(feedURL) - \(error)")
completion()
return
}
guard let feed = account?.existingFeed(withURL: feedURL) else {
completion()
return
}
processFeed(feed, response, data, completion)
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedForFeedURL: String) {
refreshProgress.completeTask()
}
}
private extension LocalAccountDelegate {
@@ -288,9 +318,36 @@ private extension LocalAccountDelegate {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
}
}
}
func processFeed(_ feed: Feed, _ response: URLResponse?, _ data: Data, _ completion: @escaping () -> Void) {
let dataHash = data.md5String
if dataHash == feed.contentHash {
completion()
return
}
let parserData = ParserData(url: feed.url, data: data)
FeedParser.parse(parserData) { (parsedFeed, error) in
Task { @MainActor in
guard let account = self.account, let parsedFeed = parsedFeed, error == nil else {
completion()
return
}
account.update(feed, with: parsedFeed) { result in
if case .success(_) = result {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
}
completion()
}
}
}
}
}

View File

@@ -591,10 +591,11 @@ private extension CloudKitAccountDelegate {
func combinedRefresh(_ account: Account, _ feeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
let feedURLs = Set(feeds.map{ $0.url })
let group = DispatchGroup()
group.enter()
refresher.refreshFeeds(feeds) {
refresher.refreshFeedURLs(feedURLs) {
group.leave()
}
@@ -825,17 +826,85 @@ private extension CloudKitAccountDelegate {
}
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestForFeedURL feedURL: String) -> URLRequest? {
guard let url = URL(string: feedURL) else {
return nil
}
var request = URLRequest(url: url)
if let feed = account?.existingFeed(withURL: feedURL) {
feed.conditionalGetInfo?.addRequestHeadersToURLRequest(&request)
}
return request
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, feedURL: String, response: URLResponse?, data: Data, error: Error?, completion: @escaping () -> Void) {
guard !data.isEmpty else {
completion()
return
}
if let error {
print("Error downloading \(feedURL) - \(error)")
completion()
return
}
guard let feed = account?.existingFeed(withURL: feedURL) else {
completion()
return
}
processFeed(feed, response, data, completion)
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedForFeedURL: String) {
refreshProgress.completeTask()
}
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void) {
self.storeArticleChanges(new: articleChanges.newArticles,
updated: articleChanges.updatedArticles,
deleted: articleChanges.deletedArticles,
completion: completion)
}
}
private extension CloudKitAccountDelegate {
func processFeed(_ feed: Feed, _ response: URLResponse?, _ data: Data, _ completion: @escaping () -> Void) {
let dataHash = data.md5String
if dataHash == feed.contentHash {
completion()
return
}
let parserData = ParserData(url: feed.url, data: data)
FeedParser.parse(parserData) { (parsedFeed, error) in
Task { @MainActor in
guard let account = self.account, let parsedFeed = parsedFeed, error == nil else {
completion()
return
}
account.update(feed, with: parsedFeed) { result in
switch result {
case .success(let articleChanges):
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
self.storeArticleChanges(new: articleChanges.newArticles,
updated: articleChanges.updatedArticles,
deleted: articleChanges.deletedArticles,
completion: completion)
case .failure:
completion()
}
}
}
}
}
}

View File

@@ -1,172 +0,0 @@
//
// LocalAccountRefresher.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSParser
import RSWeb
import Articles
import ArticlesDatabase
protocol LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed)
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void)
}
final class LocalAccountRefresher {
private var completion: (() -> Void)? = nil
private var isSuspended = false
var delegate: LocalAccountRefresherDelegate?
private lazy var downloadSession: DownloadSession = {
return DownloadSession(delegate: self)
}()
public func refreshFeeds(_ feeds: Set<Feed>, completion: (() -> Void)? = nil) {
guard !feeds.isEmpty else {
completion?()
return
}
self.completion = completion
downloadSession.downloadObjects(feeds as NSSet)
}
public func suspend() {
downloadSession.cancelAll()
isSuspended = true
}
public func resume() {
isSuspended = false
}
}
// MARK: - DownloadSessionDelegate
extension LocalAccountRefresher: DownloadSessionDelegate {
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
guard let feed = representedObject as? Feed else {
return nil
}
guard let url = URL(string: feed.url) else {
return nil
}
var request = URLRequest(url: url)
if let conditionalGetInfo = feed.conditionalGetInfo {
conditionalGetInfo.addRequestHeadersToURLRequest(&request)
}
return request
}
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
let feed = representedObject as! Feed
guard !data.isEmpty, !isSuspended else {
completion()
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
if let error = error {
print("Error downloading \(feed.url) - \(error)")
completion()
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
let dataHash = data.md5String
if dataHash == feed.contentHash {
completion()
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
let parserData = ParserData(url: feed.url, data: data)
FeedParser.parse(parserData) { (parsedFeed, error) in
Task { @MainActor in
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return
}
account.update(feed, with: parsedFeed) { result in
if case .success(let articleChanges) = result {
if let httpResponse = response as? HTTPURLResponse {
feed.conditionalGetInfo = HTTPConditionalGetInfo(urlResponse: httpResponse)
}
feed.contentHash = dataHash
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
completion()
}
} else {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
}
}
}
}
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
let feed = representedObject as! Feed
guard !isSuspended else {
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false
}
if data.isEmpty {
return true
}
if data.isDefinitelyNotFeed() {
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
return false
}
return true
}
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
let feed = representedObject as! Feed
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
}
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
completion?()
completion = nil
}
}
// MARK: - Utility
private extension Data {
func isDefinitelyNotFeed() -> Bool {
// We only detect a few image types for now. This should get fleshed-out at some later date.
return self.isImage
}
}

View File

@@ -13,17 +13,20 @@ let package = Package(
targets: ["LocalAccount"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
],
targets: [
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "LocalAccount",
dependencies: [
"RSParser",
"RSWeb"]
"RSCore",
"RSWeb",
"RSParser"
]
),
.testTarget(
name: "LocalAccountTests",

View File

@@ -0,0 +1,122 @@
//
// LocalAccountRefresher.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSWeb
public protocol LocalAccountRefresherDelegate {
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestForFeedURL: String) -> URLRequest?
func localAccountRefresher(_ refresher: LocalAccountRefresher, feedURL: String, response: URLResponse?, data: Data, error: Error?, completion: @escaping () -> Void)
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedForFeedURL: String)
}
public final class LocalAccountRefresher {
private var completion: (() -> Void)? = nil
private var isSuspended = false
public var delegate: LocalAccountRefresherDelegate?
private lazy var downloadSession: DownloadSession = {
return DownloadSession(delegate: self)
}()
public init() {}
public func refreshFeedURLs(_ feedURLs: Set<String>, completion: (() -> Void)? = nil) {
guard !feedURLs.isEmpty else {
completion?()
return
}
self.completion = completion
downloadSession.downloadObjects(feedURLs as NSSet)
}
public func suspend() {
downloadSession.cancelAll()
isSuspended = true
}
public func resume() {
isSuspended = false
}
}
// MARK: - DownloadSessionDelegate
extension LocalAccountRefresher: DownloadSessionDelegate {
public func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
let feedURL = representedObject as! String
return delegate?.localAccountRefresher(self, requestForFeedURL: feedURL)
}
public func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
guard !isSuspended else {
completion()
return
}
let feedURL = representedObject as! String
delegate?.localAccountRefresher(self, feedURL: feedURL, response: response, data: data, error: error) {
completion()
self.delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
}
}
public func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
let feedURL = representedObject as! String
guard !isSuspended else {
delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
return false
}
if data.isEmpty {
return true
}
if data.isDefinitelyNotFeed() {
delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
return false
}
return true
}
public func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
let feedURL = representedObject as! String
delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
}
public func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
let feedURL = representedObject as! String
delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
}
public func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
let feedURL = representedObject as! String
delegate?.localAccountRefresher(self, requestCompletedForFeedURL: feedURL)
}
public func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
completion?()
completion = nil
}
}
// MARK: - Utility
private extension Data {
func isDefinitelyNotFeed() -> Bool {
// We only detect a few image types for now. This should get fleshed-out at some later date.
return self.isImage
}
}