Collapse the articles and statuses into a single CKRecord

This commit is contained in:
Maurice Parker 2020-04-26 04:28:42 -05:00
parent 9eef9ea8dc
commit 71788d8f69
4 changed files with 72 additions and 106 deletions

View File

@ -112,7 +112,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
func processWithArticles(_ articles: Set<Article>) {
self.articlesZone.modifyArticlesAndStatuses(syncStatuses, articles: articles) { result in
self.articlesZone.modifyArticles(syncStatuses, articles: articles) { result in
switch result {
case .success:
self.database.deleteSelectedForProcessing(syncStatuses.map({ $0.articleID }) )
@ -151,7 +151,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing article statuses...")
articlesZone.refreshArticlesAndStatuses() { result in
articlesZone.refreshArticles() { result in
os_log(.debug, log: self.log, "Done refreshing article statuses.")
switch result {
case .success:
@ -616,9 +616,9 @@ private extension CloudKitAccountDelegate {
group.notify(queue: DispatchQueue.main) {
self.articlesZone.deleteArticlesAndStatuses(deletedArticles) { _ in
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.saveNewArticlesAndStatuses(newArticles) { _ in
self.articlesZone.saveNewArticles(newArticles) { _ in
self.refreshProgress.completeTask()
completion()
}
@ -665,9 +665,9 @@ private extension CloudKitAccountDelegate {
let newArticles = articleChanges.newArticles ?? Set<Article>()
let deletedArticles = articleChanges.deletedArticles ?? Set<Article>()
self.articlesZone.deleteArticlesAndStatuses(deletedArticles) { _ in
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.saveNewArticlesAndStatuses(newArticles) { _ in
self.articlesZone.saveNewArticles(newArticles) { _ in
self.refreshProgress.clear()
completion(.success(feed))
}
@ -744,9 +744,9 @@ private extension CloudKitAccountDelegate {
let newArticles = articleChanges.newArticles ?? Set<Article>()
let deletedArticles = articleChanges.deletedArticles ?? Set<Article>()
self.articlesZone.deleteArticlesAndStatuses(deletedArticles) { _ in
self.articlesZone.deleteArticles(deletedArticles) { _ in
self.refreshProgress.completeTask()
self.articlesZone.saveNewArticlesAndStatuses(newArticles) { _ in
self.articlesZone.saveNewArticles(newArticles) { _ in
self.refreshProgress.clear()
completion(.success(feed))
}

View File

@ -214,9 +214,9 @@ private extension CloudKitAcountZoneDelegate {
switch result {
case .success(let articleChanges):
self.articlesZone?.deleteArticlesAndStatuses(articleChanges.deletedArticles ?? Set<Article>()) { _ in
self.articlesZone?.deleteArticles(articleChanges.deletedArticles ?? Set<Article>()) { _ in
self.refreshProgress?.completeTask()
self.articlesZone?.saveNewArticlesAndStatuses(articleChanges.newArticles ?? Set<Article>()) { _ in
self.articlesZone?.saveNewArticles(articleChanges.newArticles ?? Set<Article>()) { _ in
self.refreshProgress?.completeTask()
completion(webFeed)
}
@ -251,9 +251,9 @@ private extension CloudKitAcountZoneDelegate {
BatchUpdate.shared.end()
switch result {
case .success(let articleChanges):
self.articlesZone?.deleteArticlesAndStatuses(articleChanges.deletedArticles ?? Set<Article>()) { _ in
self.articlesZone?.deleteArticles(articleChanges.deletedArticles ?? Set<Article>()) { _ in
self.refreshProgress?.completeTask()
self.articlesZone?.saveNewArticlesAndStatuses(articleChanges.newArticles ?? Set<Article>()) { _ in
self.articlesZone?.saveNewArticles(articleChanges.newArticles ?? Set<Article>()) { _ in
self.refreshProgress?.completeTask()
completion(webFeed)
}

View File

@ -29,7 +29,7 @@ final class CloudKitArticlesZone: CloudKitZone {
struct CloudKitArticle {
static let recordType = "Article"
struct Fields {
static let articleStatus = "articleStatus"
static let hollow = "hollow"
static let webFeedURL = "webFeedURL"
static let uniqueID = "uniqueID"
static let title = "title"
@ -42,24 +42,17 @@ final class CloudKitArticlesZone: CloudKitZone {
static let datePublished = "datePublished"
static let dateModified = "dateModified"
static let parsedAuthors = "parsedAuthors"
}
}
struct CloudKitArticleStatus {
static let recordType = "ArticleStatus"
struct Fields {
static let webFeedExternalID = "webFeedExternalID"
static let read = "read"
static let starred = "starred"
}
}
init(container: CKContainer) {
self.container = container
self.database = container.privateCloudDatabase
}
func refreshArticlesAndStatuses(completion: @escaping ((Result<Void, Error>) -> Void)) {
func refreshArticles(completion: @escaping ((Result<Void, Error>) -> Void)) {
fetchChangesInZone() { result in
switch result {
case .success:
@ -69,7 +62,7 @@ final class CloudKitArticlesZone: CloudKitZone {
self.createZoneRecord() { result in
switch result {
case .success:
self.refreshArticlesAndStatuses(completion: completion)
self.refreshArticles(completion: completion)
case .failure(let error):
completion(.failure(error))
}
@ -81,13 +74,13 @@ final class CloudKitArticlesZone: CloudKitZone {
}
}
func saveNewArticlesAndStatuses(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
}
var records = makeNewStatusRecords(articles)
var records = [CKRecord]()
for article in articles {
records.append(contentsOf: makeArticleRecords(article))
}
@ -95,30 +88,30 @@ final class CloudKitArticlesZone: CloudKitZone {
saveIfNew(records, completion: completion)
}
func deleteArticlesAndStatuses(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
func deleteArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
}
let recordIDs = articles.map { CKRecord.ID(recordName: statusID($0.articleID), zoneID: Self.zoneID) }
let recordIDs = articles.map { CKRecord.ID(recordName: $0.articleID, zoneID: Self.zoneID) }
delete(recordIDs: recordIDs, completion: completion)
}
func modifyArticlesAndStatuses(_ syncStatuses: [SyncStatus], articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
var records = makeStatusRecords(syncStatuses, articles)
func modifyArticles(_ syncStatuses: [SyncStatus], articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
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 deleteArticleIDs = articles.subtracting(saveArticles).map {
return CKRecord.ID(recordName: articleID($0.articleID), zoneID: Self.zoneID)
let hollowArticles = articles.subtracting(saveArticles)
for hollowArticle in hollowArticles {
records.append(contentsOf: makeHollowArticleRecords(hollowArticle))
}
self.modify(recordsToSave: records, recordIDsToDelete: deleteArticleIDs) { result in
self.modify(recordsToSave: records, recordIDsToDelete: []) { result in
switch result {
case .success:
completion(.success(()))
@ -126,15 +119,18 @@ final class CloudKitArticlesZone: CloudKitZone {
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: articles, completion: completion)
}
}
}
}
private extension CloudKitArticlesZone {
func handleSendArticleStatusError(_ error: Error, syncStatuses: [SyncStatus], starredArticles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.modifyArticlesAndStatuses(syncStatuses, articles: starredArticles, completion: completion)
self.modifyArticles(syncStatuses, articles: starredArticles, completion: completion)
case .failure(let error):
completion(.failure(error))
}
@ -144,74 +140,13 @@ final class CloudKitArticlesZone: CloudKitZone {
}
}
}
private extension CloudKitArticlesZone {
func statusID(_ id: String) -> String {
return "s|\(id)"
}
func articleID(_ id: String) -> String {
return "a|\(id)"
}
func makeNewStatusRecords(_ articles: Set<Article>) -> [CKRecord] {
var records = [CKRecord]()
for article in articles {
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: Self.zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
if let webFeedExternalID = article.webFeed?.externalID {
record[CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
}
record[CloudKitArticleStatus.Fields.read] = "0"
records.append(record)
}
return records
}
func makeStatusRecords(_ syncStatuses: [SyncStatus], _ articles: Set<Article>) -> [CKRecord] {
var articleDict = [String: Article]()
for article in articles {
articleDict[article.articleID] = article
}
var records = [String: CKRecord]()
for status in syncStatuses {
var record = records[status.articleID]
if record == nil {
let recordID = CKRecord.ID(recordName: statusID(status.articleID), zoneID: Self.zoneID)
record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
records[status.articleID] = record
}
if let webFeedExternalID = articleDict[status.articleID]?.webFeed?.externalID {
record![CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
}
switch status.key {
case .read:
record![CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0"
case .starred:
record![CloudKitArticleStatus.Fields.starred] = status.flag ? "1" : "0"
}
}
return Array(records.values)
}
func makeArticleRecords(_ article: Article) -> [CKRecord] {
var records = [CKRecord]()
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: Self.zoneID)
let recordID = CKRecord.ID(recordName: article.articleID, zoneID: Self.zoneID)
let articleRecord = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
let articleStatusRecordID = CKRecord.ID(recordName: article.articleID, zoneID: Self.zoneID)
articleRecord[CloudKitArticle.Fields.articleStatus] = CKRecord.Reference(recordID: articleStatusRecordID, action: .deleteSelf)
articleRecord[CloudKitArticle.Fields.hollow] = "0"
articleRecord[CloudKitArticle.Fields.webFeedURL] = article.webFeed?.url
articleRecord[CloudKitArticle.Fields.uniqueID] = article.uniqueID
articleRecord[CloudKitArticle.Fields.title] = article.title
@ -240,6 +175,35 @@ private extension CloudKitArticlesZone {
articleRecord[CloudKitArticle.Fields.parsedAuthors] = parsedAuthors
}
articleRecord[CloudKitArticle.Fields.read] = article.status.read ? "1" : "0"
articleRecord[CloudKitArticle.Fields.starred] = article.status.starred ? "1" : "0"
records.append(articleRecord)
return records
}
func makeHollowArticleRecords(_ article: Article) -> [CKRecord] {
var records = [CKRecord]()
let recordID = CKRecord.ID(recordName: article.articleID, zoneID: Self.zoneID)
let articleRecord = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
articleRecord[CloudKitArticle.Fields.hollow] = "1"
articleRecord[CloudKitArticle.Fields.webFeedURL] = article.webFeed?.url
articleRecord[CloudKitArticle.Fields.uniqueID] = nil
articleRecord[CloudKitArticle.Fields.title] = nil
articleRecord[CloudKitArticle.Fields.contentHTML] = nil
articleRecord[CloudKitArticle.Fields.contentText] = nil
articleRecord[CloudKitArticle.Fields.url] = nil
articleRecord[CloudKitArticle.Fields.externalURL] = nil
articleRecord[CloudKitArticle.Fields.summary] = nil
articleRecord[CloudKitArticle.Fields.imageURL] = nil
articleRecord[CloudKitArticle.Fields.datePublished] = nil
articleRecord[CloudKitArticle.Fields.dateModified] = nil
articleRecord[CloudKitArticle.Fields.parsedAuthors] = nil
articleRecord[CloudKitArticle.Fields.read] = article.status.read ? "1" : "0"
articleRecord[CloudKitArticle.Fields.starred] = article.status.starred ? "1" : "0"
records.append(articleRecord)
return records
}

View File

@ -62,13 +62,11 @@ private extension CloudKitArticlesZoneDelegate {
func process(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ $0.externalID }))
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ $0.externalID }))
let receivedUnstarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.starred] == "0" }).map({ $0.externalID }))
let receivedStarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.starred] == "1" }).map({ $0.externalID }))
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticle.Fields.read] == "0" }).map({ $0.externalID }))
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticle.Fields.read] == "1" }).map({ $0.externalID }))
let receivedUnstarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticle.Fields.starred] == "0" }).map({ $0.externalID }))
let receivedStarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticle.Fields.starred] == "1" }).map({ $0.externalID }))
let receivedArticles = records.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticle.recordType })
let updateableUnreadArticleIDs = receivedUnreadArticleIDs.subtracting(pendingReadStatusArticleIDs)
let updateableReadArticleIDs = receivedReadArticleIDs.subtracting(pendingReadStatusArticleIDs)
let updateableUnstarredArticleIDs = receivedUnstarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
@ -96,7 +94,7 @@ private extension CloudKitArticlesZoneDelegate {
group.leave()
}
let parsedItems = receivedArticles.compactMap { makeParsedItem($0) }
let parsedItems = records.compactMap { makeParsedItem($0) }
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
for (webFeedID, parsedItems) in webFeedIDsAndItems {
group.enter()
@ -114,6 +112,10 @@ private extension CloudKitArticlesZoneDelegate {
}
func makeParsedItem(_ articleRecord: CKRecord) -> ParsedItem? {
guard articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.hollow] as? String ?? "0" == "0" else {
return nil
}
var parsedAuthors = Set<ParsedAuthor>()
let decoder = JSONDecoder()