Enhance SyncStatus so that it can communicate new, updated, and deleted

This commit is contained in:
Maurice Parker 2020-04-27 16:41:45 -05:00
parent e6b42a8e0a
commit 6870133d60
11 changed files with 158 additions and 146 deletions

@ -112,7 +112,12 @@ final class CloudKitAccountDelegate: AccountDelegate {
func processWithArticles(_ articles: Set<Article>) { func processWithArticles(_ articles: Set<Article>) {
self.articlesZone.modifyArticles(articles) { result in let articlesDict = articles.reduce(into: [String: Article]()) { result, article in
result[article.articleID] = article
}
let statusedArticles = syncStatuses.map { ($0, articlesDict[$0.articleID]) }
self.articlesZone.modifyArticles(statusedArticles) { result in
switch result { switch result {
case .success: case .success:
self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID }) ) self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID }) )
@ -428,7 +433,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
} }
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)
@ -586,10 +591,6 @@ private extension CloudKitAccountDelegate {
func combinedRefresh(_ account: Account, _ webFeeds: Set<WebFeed>, completion: @escaping () -> Void) { func combinedRefresh(_ account: Account, _ webFeeds: Set<WebFeed>, completion: @escaping () -> Void) {
var newArticles = Set<Article>()
var updatedArticles = Set<Article>()
var deletedArticles = Set<Article>()
var refresherWebFeeds = Set<WebFeed>() var refresherWebFeeds = Set<WebFeed>()
let group = DispatchGroup() let group = DispatchGroup()
@ -605,14 +606,10 @@ private extension CloudKitAccountDelegate {
account.update(webFeed.webFeedID, with: parsedItems) { result in account.update(webFeed.webFeedID, with: parsedItems) { result in
switch result { switch result {
case .success(let articleChanges): case .success(let articleChanges):
self.storeArticleChanges(new: articleChanges.newArticles, updated: articleChanges.updatedArticles, deleted: articleChanges.deletedArticles) {
newArticles.formUnion(articleChanges.newArticles ?? Set<Article>())
updatedArticles.formUnion(articleChanges.updatedArticles ?? Set<Article>())
deletedArticles.formUnion(articleChanges.deletedArticles ?? Set<Article>())
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
group.leave() group.leave()
}
case .failure(let error): case .failure(let error):
os_log(.error, log: self.log, "CloudKit Feed refresh update error: %@.", error.localizedDescription) os_log(.error, log: self.log, "CloudKit Feed refresh update error: %@.", error.localizedDescription)
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
@ -634,14 +631,13 @@ private extension CloudKitAccountDelegate {
group.enter() group.enter()
refresher.refreshFeeds(refresherWebFeeds) { refresherNewArticles, refresherUpdatedArticles, refresherDeletedArticles in refresher.refreshFeeds(refresherWebFeeds) { refresherNewArticles, refresherUpdatedArticles, refresherDeletedArticles in
newArticles.formUnion(refresherNewArticles) self.storeArticleChanges(new: refresherNewArticles, updated: refresherUpdatedArticles, deleted: refresherDeletedArticles) {
updatedArticles.formUnion(refresherUpdatedArticles)
deletedArticles.formUnion(refresherDeletedArticles)
group.leave() group.leave()
} }
}
group.notify(queue: DispatchQueue.main) { group.notify(queue: DispatchQueue.main) {
self.processRecords(new: newArticles, updated: updatedArticles, deleted: deletedArticles) { self.refreshArticleStatus(for: account) { _ in
self.articlesZone.fetchChangesInZone() { _ in self.articlesZone.fetchChangesInZone() { _ in
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
completion() completion()
@ -651,53 +647,6 @@ private extension CloudKitAccountDelegate {
} }
func processRecords(new: Set<Article>, updated: Set<Article>, deleted: Set<Article>, completion: @escaping () -> Void) {
self.articlesZone.deleteArticles(deleted) { result in
self.refreshProgress.completeTask()
switch result {
case .success:
self.articlesZone.modifyArticles(updated) { result in
self.refreshProgress.completeTask()
switch result {
case .success:
self.saveNewArticles(new) {
completion()
}
case .failure(let error):
os_log(.error, log: self.log, "CloudKit modify articles error: %@.", error.localizedDescription)
completion()
}
}
case .failure(let error):
os_log(.error, log: self.log, "CloudKit delete articles error: %@.", error.localizedDescription)
completion()
}
}
}
func saveNewArticles(_ articles: Set<Article>, completion: @escaping () -> Void) {
let group = DispatchGroup()
let articleGroups = Array(articles).chunked(into: 300).map { Set($0) }
refreshProgress.addToNumberOfTasksAndRemaining(articleGroups.count)
for articleGroup in articleGroups {
group.enter()
self.articlesZone.saveNewArticles(articleGroup) { result in
self.refreshProgress.completeTask()
group.leave()
if case .failure(let error) = result {
os_log(.error, log: self.log, "CloudKit new articles error: %@.", error.localizedDescription)
}
}
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
func createProviderWebFeed(for account: Account, urlComponents: URLComponents, editedName: String?, container: Container, feedProvider: FeedProvider, completion: @escaping (Result<WebFeed, Error>) -> Void) { func createProviderWebFeed(for account: Account, urlComponents: URLComponents, editedName: String?, container: Container, feedProvider: FeedProvider, completion: @escaping (Result<WebFeed, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6) refreshProgress.addToNumberOfTasksAndRemaining(6)
@ -731,21 +680,7 @@ private extension CloudKitAccountDelegate {
account.update(urlString, with: parsedItems) { result in account.update(urlString, with: parsedItems) { result in
switch result { switch result {
case .success: case .success:
self.sendNewArticlesToTheCloud(account, feed, completion: completion)
account.fetchArticlesAsync(.webFeed(feed)) { result in
switch result {
case .success(let articles):
self.processRecords(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
self.articlesZone.fetchChangesInZone() { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error): case .failure(let error):
self.refreshProgress.clear() self.refreshProgress.clear()
completion(.failure(error)) completion(.failure(error))
@ -812,23 +747,8 @@ private extension CloudKitAccountDelegate {
self.refreshProgress.completeTask() self.refreshProgress.completeTask()
switch result { switch result {
case .success(let externalID): case .success(let externalID):
feed.externalID = externalID feed.externalID = externalID
self.sendNewArticlesToTheCloud(account, feed, completion: completion)
account.fetchArticlesAsync(.webFeed(feed)) { result in
switch result {
case .success(let articles):
self.processRecords(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
self.articlesZone.fetchChangesInZone() { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error): case .failure(let error):
self.refreshProgress.clear() self.refreshProgress.clear()
completion(.failure(error)) completion(.failure(error))
@ -859,6 +779,31 @@ private extension CloudKitAccountDelegate {
} }
} }
func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
account.fetchArticlesAsync(.webFeed(feed)) { result in
switch result {
case .success(let articles):
self.storeArticleChanges(new: articles, updated: Set<Article>(), deleted: Set<Article>()) {
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.articlesZone.fetchChangesInZone() { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
}
case .failure(let error):
self.refreshProgress.clear()
completion(.failure(error))
}
}
}
func processAccountError(_ account: Account, _ error: Error) { func processAccountError(_ account: Account, _ error: Error) {
if case CloudKitZoneError.userDeletedZone = error { if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelWebFeeds) account.removeFeeds(account.topLevelWebFeeds)
@ -868,6 +813,42 @@ private extension CloudKitAccountDelegate {
} }
} }
func storeArticleChanges(new: Set<Article>?, updated: Set<Article>?, deleted: Set<Article>?, completion: @escaping () -> Void) {
let group = DispatchGroup()
group.enter()
insertSyncStatuses(articles: new, statusKey: .new, flag: true) {
group.leave()
}
group.enter()
insertSyncStatuses(articles: updated, statusKey: .new, flag: false) {
group.leave()
}
group.enter()
insertSyncStatuses(articles: deleted, statusKey: .deleted, flag: true) {
group.leave()
}
group.notify(queue: DispatchQueue.main) {
completion()
}
}
func insertSyncStatuses(articles: Set<Article>?, statusKey: SyncStatus.Key, flag: Bool, completion: @escaping () -> Void) {
guard let articles = articles else {
completion()
return
}
let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag)
}
database.insertStatuses(syncStatuses) { _ in
completion()
}
}
} }
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate { extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {

@ -12,6 +12,7 @@ import RSParser
import RSWeb import RSWeb
import CloudKit import CloudKit
import Articles import Articles
import SyncDatabase
final class CloudKitArticlesZone: CloudKitZone { final class CloudKitArticlesZone: CloudKitZone {
@ -95,40 +96,50 @@ final class CloudKitArticlesZone: CloudKitZone {
delete(ckQuery: ckQuery, completion: completion) delete(ckQuery: ckQuery, completion: completion)
} }
func deleteArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) { func modifyArticles(_ statusArticles: [(status: SyncStatus, article: Article?)], completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else { guard !statusArticles.isEmpty else {
completion(.success(())) completion(.success(()))
return return
} }
let recordIDs = articles.map { CKRecord.ID(recordName: $0.articleID, zoneID: Self.zoneID) } var newRecords = [CKRecord]()
delete(recordIDs: recordIDs, completion: completion) var modifyRecords = [CKRecord]()
var deleteRecordIDs = [CKRecord.ID]()
for statusArticle in statusArticles {
switch (statusArticle.status.key, statusArticle.status.flag) {
case (.new, true):
// create status
if let article = statusArticle.article {
newRecords.append(contentsOf: makeArticleRecords(article))
}
case (.starred, true), (.read, false):
// create status
if let article = statusArticle.article {
modifyRecords.append(contentsOf: makeArticleRecords(article))
}
case (.deleted, true):
deleteRecordIDs.append(CKRecord.ID(recordName: statusArticle.status.articleID, zoneID: Self.zoneID))
default:
print()
// create status
// delete article record
}
} }
func modifyArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) { saveIfNew(newRecords) { result in
guard !articles.isEmpty else { switch result {
completion(.success(())) case .success:
return self.modify(recordsToSave: modifyRecords, recordIDsToDelete: deleteRecordIDs) { result in
}
var records = [CKRecord]()
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
for saveArticle in saveArticles {
records.append(contentsOf: makeArticleRecords(saveArticle))
}
let hollowArticles = articles.subtracting(saveArticles)
for hollowArticle in hollowArticles {
records.append(contentsOf: makeHollowArticleRecords(hollowArticle))
}
self.modify(recordsToSave: records, recordIDsToDelete: []) { result in
switch result { switch result {
case .success: case .success:
completion(.success(())) completion(.success(()))
case .failure(let error): case .failure(let error):
self.handleSendArticleStatusError(error, articles: articles, completion: completion) self.handleSendArticleStatusError(error, statusArticles: statusArticles, completion: completion)
}
}
case .failure(let error):
self.handleSendArticleStatusError(error, statusArticles: statusArticles, completion: completion)
} }
} }
} }
@ -137,12 +148,12 @@ final class CloudKitArticlesZone: CloudKitZone {
private extension CloudKitArticlesZone { private extension CloudKitArticlesZone {
func handleSendArticleStatusError(_ error: Error, articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) { func handleSendArticleStatusError(_ error: Error, statusArticles: [(status: SyncStatus, article: Article?)], completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error { if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in self.createZoneRecord() { result in
switch result { switch result {
case .success: case .success:
self.modifyArticles(articles, completion: completion) self.modifyArticles(statusArticles, completion: completion)
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }

@ -261,9 +261,12 @@ final class FeedWranglerAPICaller: NSObject {
switch status.key { switch status.key {
case .read: case .read:
return URLQueryItem(name: "read", value: status.flag.description) return URLQueryItem(name: "read", value: status.flag.description)
case .starred: case .starred:
return URLQueryItem(name: "starred", value: status.flag.description) return URLQueryItem(name: "starred", value: status.flag.description)
case .deleted:
return nil
case .new:
return nil
} }
} }
queryItems.append(URLQueryItem(name: "feed_item_id", value: articleID)) queryItems.append(URLQueryItem(name: "feed_item_id", value: articleID))

@ -436,7 +436,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
} }
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { SyncStatus(articleID: $0.articleID, key: statusKey, flag: flag)} let syncStatuses = articles.map { SyncStatus(articleID: $0.articleID, key: SyncStatus.Key(statusKey), flag: flag)}
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)
database.selectPendingCount { result in database.selectPendingCount { result in

@ -119,10 +119,10 @@ final class FeedbinAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == false } let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == true } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == true } let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false } let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup() let group = DispatchGroup()
var errorOccurred = false var errorOccurred = false
@ -537,7 +537,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
} }
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)

@ -489,7 +489,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
} }
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)

@ -47,7 +47,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation {
private extension FeedlySendArticleStatusesOperation { private extension FeedlySendArticleStatusesOperation {
func processStatuses(_ pending: [SyncStatus]) { func processStatuses(_ pending: [SyncStatus]) {
let statuses: [(status: ArticleStatus.Key, flag: Bool, action: FeedlyMarkAction)] = [ let statuses: [(status: SyncStatus.Key, flag: Bool, action: FeedlyMarkAction)] = [
(.read, false, .unread), (.read, false, .unread),
(.read, true, .read), (.read, true, .read),
(.starred, true, .saved), (.starred, true, .saved),

@ -131,16 +131,16 @@ final class NewsBlurAccountDelegate: AccountDelegate {
func processStatuses(_ syncStatuses: [SyncStatus]) { func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { let createUnreadStatuses = syncStatuses.filter {
$0.key == ArticleStatus.Key.read && $0.flag == false $0.key == SyncStatus.Key.read && $0.flag == false
} }
let deleteUnreadStatuses = syncStatuses.filter { let deleteUnreadStatuses = syncStatuses.filter {
$0.key == ArticleStatus.Key.read && $0.flag == true $0.key == SyncStatus.Key.read && $0.flag == true
} }
let createStarredStatuses = syncStatuses.filter { let createStarredStatuses = syncStatuses.filter {
$0.key == ArticleStatus.Key.starred && $0.flag == true $0.key == SyncStatus.Key.starred && $0.flag == true
} }
let deleteStarredStatuses = syncStatuses.filter { let deleteStarredStatuses = syncStatuses.filter {
$0.key == ArticleStatus.Key.starred && $0.flag == false $0.key == SyncStatus.Key.starred && $0.flag == false
} }
let group = DispatchGroup() let group = DispatchGroup()
@ -575,7 +575,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
} }
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)

@ -123,10 +123,10 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
database.selectForProcessing { result in database.selectForProcessing { result in
func processStatuses(_ syncStatuses: [SyncStatus]) { func processStatuses(_ syncStatuses: [SyncStatus]) {
let createUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == false } let createUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == false }
let deleteUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == true } let deleteUnreadStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.read && $0.flag == true }
let createStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == true } let createStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == true }
let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false } let deleteStarredStatuses = syncStatuses.filter { $0.key == SyncStatus.Key.starred && $0.flag == false }
let group = DispatchGroup() let group = DispatchGroup()
@ -412,7 +412,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? { func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let syncStatuses = articles.map { article in let syncStatuses = articles.map { article in
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
} }
database.insertStatuses(syncStatuses) database.insertStatuses(syncStatuses)

@ -12,12 +12,29 @@ import RSDatabase
public struct SyncStatus: Hashable, Equatable { public struct SyncStatus: Hashable, Equatable {
public enum Key: String {
case read = "read"
case starred = "starred"
case deleted = "deleted"
case new = "new"
public init(_ articleStatusKey: ArticleStatus.Key) {
switch articleStatusKey {
case .read:
self = Self.read
case .starred:
self = Self.starred
}
}
}
public let articleID: String public let articleID: String
public let key: ArticleStatus.Key public let key: SyncStatus.Key
public let flag: Bool public let flag: Bool
public let selected: Bool public let selected: Bool
public init(articleID: String, key: ArticleStatus.Key, flag: Bool, selected: Bool = false) { public init(articleID: String, key: SyncStatus.Key, flag: Bool, selected: Bool = false) {
self.articleID = articleID self.articleID = articleID
self.key = key self.key = key
self.flag = flag self.flag = flag

@ -155,7 +155,7 @@ private extension SyncStatusTable {
func statusWithRow(_ row: FMResultSet) -> SyncStatus? { func statusWithRow(_ row: FMResultSet) -> SyncStatus? {
guard let articleID = row.string(forColumn: DatabaseKey.articleID), guard let articleID = row.string(forColumn: DatabaseKey.articleID),
let rawKey = row.string(forColumn: DatabaseKey.key), let rawKey = row.string(forColumn: DatabaseKey.key),
let key = ArticleStatus.Key(rawValue: rawKey) else { let key = SyncStatus.Key(rawValue: rawKey) else {
return nil return nil
} }