diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index e5db38c00..1855d63c4 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -1115,7 +1115,7 @@ private extension FeedbinAccountDelegate { let parsedItems: [ParsedItem] = entries.map { entry in let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) - return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: authors, tags: nil, attachments: nil) + return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parsedDatePublished, dateModified: nil, authors: authors, tags: nil, attachments: nil) } return Set(parsedItems) diff --git a/Frameworks/Account/Feedbin/FeedbinEntry.swift b/Frameworks/Account/Feedbin/FeedbinEntry.swift index 8d73a51da..82b40e186 100644 --- a/Frameworks/Account/Feedbin/FeedbinEntry.swift +++ b/Frameworks/Account/Feedbin/FeedbinEntry.swift @@ -10,7 +10,7 @@ import Foundation import RSParser import RSCore -struct FeedbinEntry: Codable { +final class FeedbinEntry: Codable { let articleID: Int let feedID: Int @@ -23,6 +23,19 @@ struct FeedbinEntry: Codable { let dateArrived: String? let jsonFeed: FeedbinEntryJSONFeed? + // Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin + // requires a very specific date formatter to work and even then it fails occasionally. + // Rather than loose all the entries we only lose the one date by decoding as a string + // and letting the one date fail when parsed. + lazy var parsedDatePublished: Date? = { + if let datePublished = datePublished { + return RSDateWithString(datePublished) + } + else { + return nil + } + }() + enum CodingKeys: String, CodingKey { case articleID = "id" case feedID = "feed_id" @@ -35,19 +48,6 @@ struct FeedbinEntry: Codable { case dateArrived = "created_at" case jsonFeed = "json_feed" } - - // Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin - // requires a very specific date formatter to work and even then it fails occasionally. - // Rather than loose all the entries we only lose the one date by decoding as a string - // and letting the one date fail when parsed. - func parseDatePublished() -> Date? { - if datePublished != nil { - return FeedbinDate.formatter.date(from: datePublished!) - } else { - return nil - } - } - } struct FeedbinEntryJSONFeed: Codable { diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 252da0098..894642799 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -20,6 +20,7 @@ final class ArticlesTable: DatabaseTable { private let statusesTable: StatusesTable private let authorsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable + private var databaseArticlesCache = [String: DatabaseArticle]() private lazy var searchTable: SearchTable = { return SearchTable(queue: queue, articlesTable: self) @@ -482,16 +483,21 @@ private extension ArticlesTable { func makeDatabaseArticles(with resultSet: FMResultSet) -> Set { let articles = resultSet.mapToSet { (row) -> DatabaseArticle? in - // The resultSet is a result of a JOIN query with the statuses table, - // so we can get the statuses at the same time and avoid additional database lookups. - - guard let status = statusesTable.statusWithRow(resultSet) else { - assertionFailure("Expected status.") + guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { + assertionFailure("Expected articleID.") return nil } - guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { - assertionFailure("Expected articleID.") + // Articles are removed from the cache when they’re updated. + // See saveUpdatedArticles. + if let databaseArticle = databaseArticlesCache[articleID] { + return databaseArticle + } + + // The resultSet is a result of a JOIN query with the statuses table, + // so we can get the statuses at the same time and avoid additional database lookups. + guard let status = statusesTable.statusWithRow(resultSet, articleID: articleID) else { + assertionFailure("Expected status.") return nil } guard let feedID = row.string(forColumn: DatabaseKey.feedID) else { @@ -514,7 +520,9 @@ private extension ArticlesTable { let datePublished = row.date(forColumn: DatabaseKey.datePublished) let dateModified = row.date(forColumn: DatabaseKey.dateModified) - return DatabaseArticle(articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, status: status) + let databaseArticle = DatabaseArticle(articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, status: status) + databaseArticlesCache[articleID] = databaseArticle + return databaseArticle } return articles @@ -670,6 +678,7 @@ private extension ArticlesTable { func saveUpdatedArticles(_ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ database: FMDatabase) { + removeArticlesFromDatabaseArticlesCache(updatedArticles) saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database) for updatedArticle in updatedArticles { @@ -690,10 +699,17 @@ private extension ArticlesTable { // Not unexpected. There may be no changes. return } - + updateRowsWithDictionary(changesDictionary, whereKey: DatabaseKey.articleID, matches: updatedArticle.articleID, database: database) } - + + func removeArticlesFromDatabaseArticlesCache(_ updatedArticles: Set
) { + let articleIDs = updatedArticles.articleIDs() + for articleID in articleIDs { + databaseArticlesCache[articleID] = nil + } + } + func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool { // Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months). if status.userDeleted { diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 0bb18377d..fa5a741ab 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -105,17 +105,21 @@ final class StatusesTable: DatabaseTable { guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { return nil } + return statusWithRow(row, articleID: articleID) + } + + func statusWithRow(_ row: FMResultSet, articleID: String) ->ArticleStatus? { if let cachedStatus = cache[articleID] { return cachedStatus } - + guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else { return nil } let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row) cache.addStatusIfNotCached(articleStatus) - + return articleStatus } diff --git a/README.md b/README.md index 2f9cab550..020eb826d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Here’s [How to Support NetNewsWire](Technotes/HowToSupportNetNewsWire.markdown #### Community -[Join the Slack group](https://join.slack.com/t/netnewswire/shared_invite/enQtNjM4MDA1MjQzMDkzLTNlNjBhOWVhYzdhYjA4ZWFhMzQ1MTUxYjU0NTE5ZGY0YzYwZWJhNjYwNTNmNTg2NjIwYWY4YzhlYzk5NmU3ZTc) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. +[Join the Slack group](https://ranchero.com/netnewswire/slack) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. Every community member is expected to abide by the code of conduct which is included in the [Contributing](CONTRIBUTING.md) page.