Continue adoptContinue converting ArticlesDatabase to async/await.

This commit is contained in:
Brent Simmons
2023-10-04 20:59:04 -07:00
parent c0b92dfa78
commit 48f1bec744
3 changed files with 177 additions and 187 deletions

View File

@@ -765,7 +765,14 @@ public enum FetchType {
}
public func fetchUnreadCountForStarredArticles(_ completion: @escaping SingleUnreadCountCompletionBlock) {
database.fetchStarredAndUnreadCount(for: flattenedFeedIDs(), completion: completion)
Task { @MainActor in
do {
let unreadCount = try await database.unreadCountForStarredArticlesForFeedIDs(flattenedFeedIDs())
completion(.success(unreadCount))
} catch {
completion(.failure(error as! DatabaseError))
}
}
}
public func fetchCountForStarredArticles() throws -> Int {
@@ -820,14 +827,14 @@ public enum FetchType {
// Used only by an On My Mac or iCloud account.
precondition(Thread.isMainThread)
precondition(type == .onMyMac || type == .cloudKit)
database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in
switch updateArticlesResult {
case .success(let articleChanges):
Task { @MainActor in
do {
let articleChanges = try await database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder)
self.sendNotificationAbout(articleChanges)
completion(.success(articleChanges))
case .failure(let databaseError):
completion(.failure(databaseError))
} catch {
completion(.failure(error as! DatabaseError))
}
}
}
@@ -841,13 +848,13 @@ public enum FetchType {
return
}
database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
switch updateArticlesResult {
case .success(let newAndUpdatedArticles):
self.sendNotificationAbout(newAndUpdatedArticles)
Task { @MainActor in
do {
let articleChanges = try await database.update(feedIDsAndItems: feedIDsAndItems, defaultRead: defaultRead)
self.sendNotificationAbout(articleChanges)
completion(nil)
case .failure(let databaseError):
completion(databaseError)
} catch {
completion(error as? DatabaseError)
}
}
}
@@ -975,13 +982,6 @@ public enum FetchType {
}
// Delete the articles associated with the given set of articleIDs
func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
guard !articleIDs.isEmpty else {
completion?(nil)
return
}
database.delete(articleIDs: articleIDs, completion: completion)
}
/// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance.
func emptyCaches() {
@@ -1501,9 +1501,9 @@ private extension Account {
fetchingAllUnreadCounts = true
database.fetchAllUnreadCounts { result in
Task { @MainActor in
guard let unreadCountDictionary = try? result.get() else {
return
}
guard let unreadCountDictionary = try? result.get() else {
return
}
self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedFeeds())
self.fetchingAllUnreadCounts = false

View File

@@ -25,10 +25,13 @@ public typealias SingleUnreadCountResult = Result<Int, DatabaseError>
public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) -> Void
public struct ArticleChanges {
public let newArticles: Set<Article>?
public let updatedArticles: Set<Article>?
public let deletedArticles: Set<Article>?
static let empty = ArticleChanges(newArticles: nil, updatedArticles: nil, deletedArticles: nil)
public init() {
self.newArticles = Set<Article>()
self.updatedArticles = Set<Article>()
@@ -197,34 +200,28 @@ public typealias ArticleStatusesResultBlock = (ArticleStatusesResult) -> Void
try await articlesTable.unreadCountForFeedIDsSince(feedIDs, since)
}
public func fetchStarredAndUnreadCount(for feedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
articlesTable.fetchStarredAndUnreadCount(feedIDs, completion)
public func unreadCountForStarredArticlesForFeedIDs(_ feedIDs: Set<String>) async throws -> Int {
try await articlesTable.unreadCountForStarredArticlesForFeedIDs(feedIDs)
}
// MARK: - Saving, Updating, and Deleting Articles
/// Update articles and save new ones  for feed-based systems (local and iCloud).
public func update(with parsedItems: Set<ParsedItem>, feedID: String, deleteOlder: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
public func update(with parsedItems: Set<ParsedItem>, feedID: String, deleteOlder: Bool) async throws -> ArticleChanges {
precondition(retentionStyle == .feedBased)
articlesTable.update(parsedItems, feedID, deleteOlder, completion)
return try await articlesTable.update(parsedItems, feedID, deleteOlder)
}
/// Update articles and save new ones for sync systems (Feedbin, Feedly, etc.).
public func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
public func update(feedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool) async throws -> ArticleChanges {
precondition(retentionStyle == .syncSystem)
articlesTable.update(feedIDsAndItems, defaultRead, completion)
return try await articlesTable.update(feedIDsAndItems, defaultRead)
}
/// Delete articles
public func deleteArticleIDs(_ articleIDs: Set<String>) async throws {
try await articlesTable.deleteArticleIDs(articleIDs)
}
/// Delete articles
public func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock?) {
articlesTable.delete(articleIDs: articleIDs, completion: completion)
}
// MARK: - Status
/// Fetch the articleIDs of unread articles.

View File

@@ -203,11 +203,10 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Updating and Deleting
func update(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ deleteOlder: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
func update(_ parsedItems: Set<ParsedItem>, _ feedID: String, _ deleteOlder: Bool) async throws -> ArticleChanges {
precondition(retentionStyle == .feedBased)
if parsedItems.isEmpty {
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
return
return ArticleChanges.empty
}
// 1. Ensure statuses for all the incoming articles.
@@ -220,74 +219,74 @@ final class ArticlesTable: DatabaseTable {
// 8. Delete Articles in database no longer present in the feed.
// 9. Update search index.
self.queue.runInTransaction { (databaseResult) in
func makeDatabaseCalls(_ database: FMDatabase) {
let articleIDs = parsedItems.articleIDs()
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
assert(statusesDictionary.count == articleIDs.count)
let incomingArticles = Article.articlesWithParsedItems(parsedItems, feedID, self.accountID, statusesDictionary) //2
if incomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
return
}
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
// Articles to delete are 1) not starred and 2) older than 30 days and 3) no longer in feed.
let articlesToDelete: Set<Article>
if deleteOlder {
let cutoffDate = Date().bySubtracting(days: 30)
articlesToDelete = fetchedArticles.filter { (article) -> Bool in
return !article.status.starred && article.status.dateArrived < cutoffDate && !articleIDs.contains(article.articleID)
return try await withCheckedThrowingContinuation { continuation in
self.queue.runInTransaction { (databaseResult) in
func updateArticlesInDatabase(_ database: FMDatabase) -> ArticleChanges {
let articleIDs = parsedItems.articleIDs()
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) //1
assert(statusesDictionary.count == articleIDs.count)
let incomingArticles = Article.articlesWithParsedItems(parsedItems, feedID, self.accountID, statusesDictionary) //2
if incomingArticles.isEmpty {
return ArticleChanges.empty
}
} else {
articlesToDelete = Set<Article>()
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
// Articles to delete are 1) not starred and 2) older than 30 days and 3) no longer in feed.
let articlesToDelete: Set<Article>
if deleteOlder {
let cutoffDate = Date().bySubtracting(days: 30)
articlesToDelete = fetchedArticles.filter { (article) -> Bool in
return !article.status.starred && article.status.dateArrived < cutoffDate && !articleIDs.contains(article.articleID)
}
} else {
articlesToDelete = Set<Article>()
}
self.addArticlesToCache(newArticles)
self.addArticlesToCache(updatedArticles)
// 8. Delete articles no longer in feed.
let articleIDsToDelete = articlesToDelete.articleIDs()
if !articleIDsToDelete.isEmpty {
self.removeArticles(articleIDsToDelete, database)
self.removeArticleIDsFromCache(articleIDsToDelete)
}
// 9. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
}
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: articlesToDelete)
return articleChanges
}
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, articlesToDelete, completion) //7
self.addArticlesToCache(newArticles)
self.addArticlesToCache(updatedArticles)
// 8. Delete articles no longer in feed.
let articleIDsToDelete = articlesToDelete.articleIDs()
if !articleIDsToDelete.isEmpty {
self.removeArticles(articleIDsToDelete, database)
self.removeArticleIDsFromCache(articleIDsToDelete)
}
// 9. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion(.failure(databaseError))
switch databaseResult {
case .success(let database):
let articleChanges = updateArticlesInDatabase(database)
continuation.resume(returning: articleChanges)
case .failure(let databaseError):
continuation.resume(throwing: databaseError)
}
}
}
}
func update(_ feedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
func update(_ feedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool) async throws -> ArticleChanges {
precondition(retentionStyle == .syncSystem)
if feedIDsAndItems.isEmpty {
callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
return
return ArticleChanges.empty
}
// 1. Ensure statuses for all the incoming articles.
@@ -299,56 +298,56 @@ final class ArticlesTable: DatabaseTable {
// 7. Call back with new and updated Articles.
// 8. Update search index.
self.queue.runInTransaction { (databaseResult) in
return try await withCheckedThrowingContinuation { continuation in
self.queue.runInTransaction { (databaseResult) in
func makeDatabaseCalls(_ database: FMDatabase) {
var articleIDs = Set<String>()
for (_, parsedItems) in feedIDsAndItems {
articleIDs.formUnion(parsedItems.articleIDs())
func updateArticlesInDatabase(_ database: FMDatabase) -> ArticleChanges {
var articleIDs = Set<String>()
for (_, parsedItems) in feedIDsAndItems {
articleIDs.formUnion(parsedItems.articleIDs())
}
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2
if allIncomingArticles.isEmpty {
return ArticleChanges.empty
}
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
if incomingArticles.isEmpty {
return ArticleChanges.empty
}
let incomingArticleIDs = incomingArticles.articleIDs()
let fetchedArticles = self.fetchArticlesForArticleIDs(incomingArticleIDs, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
self.addArticlesToCache(newArticles)
self.addArticlesToCache(updatedArticles)
// 8. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
}
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: nil)
return articleChanges
}
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithFeedIDsAndItems(feedIDsAndItems, self.accountID, statusesDictionary) //2
if allIncomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
return
}
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
if incomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, nil, completion)
return
}
let incomingArticleIDs = incomingArticles.articleIDs()
let fetchedArticles = self.fetchArticlesForArticleIDs(incomingArticleIDs, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, nil, completion) //7
self.addArticlesToCache(newArticles)
self.addArticlesToCache(updatedArticles)
// 8. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion(.failure(databaseError))
switch databaseResult {
case .success(let database):
let articleChanges = updateArticlesInDatabase(database)
continuation.resume(returning: articleChanges)
case .failure(let databaseError):
continuation.resume(throwing: databaseError)
}
}
}
@@ -356,44 +355,19 @@ final class ArticlesTable: DatabaseTable {
public func deleteArticleIDs(_ articleIDs: Set<String>) async throws {
try await withCheckedThrowingContinuation { continuation in
Task { @MainActor in
queue.runInTransaction { databaseResult in
switch databaseResult {
case .success(let database):
self.removeArticles(articleIDs, database)
continuation.resume()
case .failure(let databaseError):
continuation.resume(throwing: databaseError)
}
}
}
}
}
public func delete(articleIDs: Set<String>, completion: DatabaseCompletionBlock?) {
self.queue.runInTransaction { (databaseResult) in
func makeDatabaseCalls(_ database: FMDatabase) {
self.removeArticles(articleIDs, database)
DispatchQueue.main.async {
completion?(nil)
try await withCheckedThrowingContinuation { continuation in
queue.runInTransaction { databaseResult in
switch databaseResult {
case .success(let database):
self.removeArticles(articleIDs, database)
continuation.resume()
case .failure(let databaseError):
continuation.resume(throwing: databaseError)
}
}
switch databaseResult {
case .success(let database):
makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion?(databaseError)
}
}
}
}
// MARK: - Unread Counts
func unreadCountForFeedID(_ feedID: String) async throws -> Int {
@@ -491,6 +465,34 @@ final class ArticlesTable: DatabaseTable {
}
}
func unreadCountForStarredArticlesForFeedIDs(_ feedIDs: Set<String>) async throws -> Int {
if feedIDs.isEmpty {
return 0
}
return try await withCheckedThrowingContinuation { continuation in
queue.runInDatabase { databaseResult in
func fetchUnreadCount(_ database: FMDatabase) -> Int {
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1;"
let parameters = Array(feedIDs) as [Any]
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
return unreadCount
}
switch databaseResult {
case .success(let database):
let unreadCount = fetchUnreadCount(database)
continuation.resume(returning: unreadCount)
case .failure(let databaseError):
continuation.resume(throwing: databaseError)
}
}
}
}
func fetchStarredAndUnreadCount(_ feedIDs: Set<String>, _ completion: @escaping SingleUnreadCountCompletionBlock) {
if feedIDs.isEmpty {
completion(.success(0))
@@ -1073,15 +1075,6 @@ private extension ArticlesTable {
return articles.filter{ articleIDs.contains($0.articleID) }
}
// MARK: - Saving Parsed Items
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ deletedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesCompletionBlock) {
let articleChanges = ArticleChanges(newArticles: newArticles, updatedArticles: updatedArticles, deletedArticles: deletedArticles)
DispatchQueue.main.async {
completion(.success(articleChanges))
}
}
// MARK: - Saving New Articles
func findNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {