From 2c58bd31d7f89b19b15e41eb925aae6e792d6e36 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 21:12:16 -0800 Subject: [PATCH 1/9] Update to latest RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 807bb445f..573bf1d99 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 807bb445f0bc692cb6b9210f66db82a85efe5c59 +Subproject commit 573bf1d99418b218d264105df620163832c36c34 From 6fdaf773cdb3f62a4b7df8821e2fac7707a8488d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 21:49:24 -0800 Subject: [PATCH 2/9] Add SyncDatabase to referenced frameworks in project settings. --- Frameworks/Account/Account.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index e9264a54c..8775622f4 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */; }; 844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844B2980210CE3BF004020B3 /* RSWeb.framework */; }; 8469F81C1F6DD15E0084783E /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935101F62486800CEBD24 /* Account.swift */; }; + 846CA1882392349E00B55117 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846CA1872392349E00B55117 /* SyncDatabase.framework */; }; 846E77451F6EF9B900A165E2 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419740D1F6DD25F006346C4 /* Container.swift */; }; 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742C1F6DDE84006346C4 /* LocalAccountDelegate.swift */; }; 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; }; @@ -153,6 +154,7 @@ 844B297C2106C7EC004020B3 /* Feed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = ""; }; 844B2980210CE3BF004020B3 /* RSWeb.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSWeb.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 846CA1872392349E00B55117 /* SyncDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 846E77531F6F00E300A165E2 /* AccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; 848934F61F62484F00CEBD24 /* Account.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Account.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 848934FA1F62484F00CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -184,6 +186,7 @@ 844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */, 841D4D722106B40A00DD04E6 /* Articles.framework in Frameworks */, 841D4D702106B40400DD04E6 /* ArticlesDatabase.framework in Frameworks */, + 846CA1882392349E00B55117 /* SyncDatabase.framework in Frameworks */, 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */, 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */, ); @@ -285,6 +288,7 @@ 8469F80F1F6DC3C10084783E /* Frameworks */ = { isa = PBXGroup; children = ( + 846CA1872392349E00B55117 /* SyncDatabase.framework */, 84EAC4812148CC6300F154AB /* RSDatabase.framework */, 844B2980210CE3BF004020B3 /* RSWeb.framework */, 841D4D712106B40A00DD04E6 /* Articles.framework */, From 9dc3ed03c78b10dcc3b5ff937b116ffa0be96a38 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 21:49:44 -0800 Subject: [PATCH 3/9] Adopt DatabaseQueue. --- .../ArticlesDatabase/ArticlesDatabase.swift | 29 ++++++++++-- .../ArticlesDatabase/ArticlesTable.swift | 30 ++++++------- Frameworks/ArticlesDatabase/SearchTable.swift | 6 +-- .../ArticlesDatabase/StatusesTable.swift | 6 +-- Frameworks/SyncDatabase/SyncDatabase.swift | 45 +++++++++++++------ Frameworks/SyncDatabase/SyncStatusTable.swift | 33 +++++--------- 6 files changed, 89 insertions(+), 60 deletions(-) diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index e14dac4de..262d76ec2 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -22,21 +22,29 @@ public typealias UpdateArticlesCompletionBlock = (Set
?, Set
?) public final class ArticlesDatabase { + /// When ArticlesDatabase is suspended, database calls will crash the app. + public var isSuspended: Bool { + return queue.isSuspended + } + private let articlesTable: ArticlesTable + private let queue: DatabaseQueue public init(databaseFilePath: String, accountID: String) { - let queue = RSDatabaseQueue(filepath: databaseFilePath, excludeFromBackup: false) + let queue = DatabaseQueue(databasePath: databaseFilePath) + self.queue = queue self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue) - queue.createTables(usingStatements: ArticlesDatabase.tableCreationStatements) - queue.update { (database) in + queue.runCreateStatements(ArticlesDatabase.tableCreationStatements) + queue.runInDatabase { database in if !self.articlesTable.containsColumn("searchRowID", in: database) { database.executeStatements("ALTER TABLE articles add column searchRowID INTEGER;") } database.executeStatements("CREATE INDEX if not EXISTS articles_searchRowID on articles(searchRowID);") database.executeStatements("DROP TABLE if EXISTS tags;DROP INDEX if EXISTS tags_tagName_index;DROP INDEX if EXISTS articles_feedID_index;DROP INDEX if EXISTS statuses_read_index;") } - queue.vacuumIfNeeded() + + queue.vacuumIfNeeded(daysBetweenVacuums: 9) DispatchQueue.main.async { self.articlesTable.indexUnindexedArticles() } @@ -145,6 +153,19 @@ public final class ArticlesDatabase { return articlesTable.mark(articles, statusKey, flag) } + // MARK: - Suspend and Resume (for iOS) + + /// Close the database and stop running database calls. + /// Any pending calls will complete first. + public func suspend() { + queue.suspend() + } + + /// Open the database and allow for running database calls again. + public func resume() { + queue.resume() + } + // MARK: - Caches /// Call to free up some memory. Should be done when the app is backgrounded, for instance. diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index a0b6edbd2..ccb912ffa 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -16,7 +16,7 @@ final class ArticlesTable: DatabaseTable { let name: String private let accountID: String - private let queue: RSDatabaseQueue + private let queue: DatabaseQueue private let statusesTable: StatusesTable private let authorsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable @@ -32,7 +32,7 @@ final class ArticlesTable: DatabaseTable { private typealias ArticlesFetchMethod = (FMDatabase) -> Set
- init(name: String, accountID: String, queue: RSDatabaseQueue) { + init(name: String, accountID: String, queue: DatabaseQueue) { self.name = name self.accountID = accountID @@ -149,7 +149,7 @@ final class ArticlesTable: DatabaseTable { func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set) -> Set
{ var articles: Set
= Set
() - queue.fetchSync { (database) in + queue.runInDatabaseSync { (database) in articles = self.fetchArticlesMatching(searchString, database) } articles = articles.filter{ feedIDs.contains($0.feedID) } @@ -229,7 +229,7 @@ final class ArticlesTable: DatabaseTable { articleIDs.formUnion(parsedItems.articleIDs()) } - self.queue.update { (database) in + self.queue.runInTransaction { (database) in let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 assert(statusesDictionary.count == articleIDs.count) @@ -273,7 +273,7 @@ final class ArticlesTable: DatabaseTable { } func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool) { - self.queue.update { (database) in + self.queue.runInTransaction { (database) in let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) let statuses = Set(statusesDictionary.values) self.statusesTable.mark(statuses, statusKey, flag, database) @@ -290,7 +290,7 @@ final class ArticlesTable: DatabaseTable { var unreadCountDictionary = UnreadCountDictionary() - queue.fetch { (database) in + queue.runInDatabase { (database) in for feedID in feedIDs { unreadCountDictionary[feedID] = self.fetchUnreadCount(feedID, database) } @@ -309,7 +309,7 @@ final class ArticlesTable: DatabaseTable { return } - queue.fetch { (database) in + queue.runInDatabase { (database) in let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0;" @@ -331,7 +331,7 @@ final class ArticlesTable: DatabaseTable { let cutoffDate = articleCutoffDate - queue.fetch { (database) in + queue.runInDatabase { (database) in let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;" guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else { @@ -361,7 +361,7 @@ final class ArticlesTable: DatabaseTable { return } - queue.fetch { (database) in + queue.runInDatabase { (database) in 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 and userDeleted=0;" let parameters = Array(feedIDs) as [Any] @@ -390,7 +390,7 @@ final class ArticlesTable: DatabaseTable { func mark(_ articles: Set
, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set? { var statuses: Set? - self.queue.updateSync { (database) in + self.queue.runInTransactionSync { (database) in statuses = self.statusesTable.mark(articles.statuses(), statusKey, flag, database) } return statuses @@ -399,7 +399,7 @@ final class ArticlesTable: DatabaseTable { // MARK: - Indexing func indexUnindexedArticles() { - queue.fetch { (database) in + queue.runInDatabase { (database) in let sql = "select articleID from articles where searchRowID is null limit 500;" guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { return @@ -419,7 +419,7 @@ final class ArticlesTable: DatabaseTable { // MARK: - Caches func emptyCaches() { - queue.run { _ in + queue.runInDatabase { _ in self.databaseArticlesCache = [String: DatabaseArticle]() } } @@ -433,7 +433,7 @@ final class ArticlesTable: DatabaseTable { if feedIDs.isEmpty { return } - queue.run { (database) in + queue.runInDatabase { (database) in let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! let sql = "select articleID from articles where feedID not in \(placeholders);" let parameters = Array(feedIDs) as [Any] @@ -458,14 +458,14 @@ private extension ArticlesTable { private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set
{ var articles = Set
() - queue.fetchSync { (database) in + queue.runInDatabaseSync { (database) in articles = fetchMethod(database) } return articles } private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ callback: @escaping ArticleSetBlock) { - queue.fetch { (database) in + queue.runInDatabase { (database) in let articles = fetchMethod(database) DispatchQueue.main.async { callback(articles) diff --git a/Frameworks/ArticlesDatabase/SearchTable.swift b/Frameworks/ArticlesDatabase/SearchTable.swift index d0c95838a..8007b366d 100644 --- a/Frameworks/ArticlesDatabase/SearchTable.swift +++ b/Frameworks/ArticlesDatabase/SearchTable.swift @@ -61,10 +61,10 @@ final class ArticleSearchInfo: Hashable { final class SearchTable: DatabaseTable { let name = "search" - private let queue: RSDatabaseQueue + private let queue: DatabaseQueue private weak var articlesTable: ArticlesTable? - init(queue: RSDatabaseQueue, articlesTable: ArticlesTable) { + init(queue: DatabaseQueue, articlesTable: ArticlesTable) { self.queue = queue self.articlesTable = articlesTable } @@ -73,7 +73,7 @@ final class SearchTable: DatabaseTable { if articleIDs.isEmpty { return } - queue.update { (database) in + queue.runInTransaction { (database) in self.ensureIndexedArticles(articleIDs, database) } } diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 65615c1b0..745bd9efb 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -19,9 +19,9 @@ final class StatusesTable: DatabaseTable { let name = DatabaseTableName.statuses private let cache = StatusCache() - private let queue: RSDatabaseQueue + private let queue: DatabaseQueue - init(queue: RSDatabaseQueue) { + init(queue: DatabaseQueue) { self.queue = queue } @@ -88,7 +88,7 @@ final class StatusesTable: DatabaseTable { func fetchArticleIDs(_ sql: String) -> Set { var articleIDs = Set() - queue.fetchSync { (database) in + queue.runInDatabaseSync { (database) in guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { return } diff --git a/Frameworks/SyncDatabase/SyncDatabase.swift b/Frameworks/SyncDatabase/SyncDatabase.swift index 39a781391..6545c84f1 100644 --- a/Frameworks/SyncDatabase/SyncDatabase.swift +++ b/Frameworks/SyncDatabase/SyncDatabase.swift @@ -9,20 +9,27 @@ import Foundation import RSDatabase -public final class SyncDatabase { - - private let syncStatusTable: SyncStatusTable - - public init(databaseFilePath: String) { - - let queue = RSDatabaseQueue(filepath: databaseFilePath, excludeFromBackup: false) - self.syncStatusTable = SyncStatusTable(queue: queue) - - queue.createTables(usingStatements: SyncDatabase.tableCreationStatements) - queue.vacuumIfNeeded() +public struct SyncDatabase { + /// When SyncDatabase is suspended, database calls will crash the app. + public var isSuspended: Bool { + return queue.isSuspended } - + + private let syncStatusTable: SyncStatusTable + private let queue: DatabaseQueue + + public init(databaseFilePath: String) { + let queue = DatabaseQueue(databasePath: databaseFilePath) + queue.runCreateStatements(SyncDatabase.tableCreationStatements) + queue.vacuumIfNeeded(daysBetweenVacuums: 11) + self.queue = queue + + self.syncStatusTable = SyncStatusTable(queue: queue) + } + + // MARK: - API + public func insertStatuses(_ statuses: [SyncStatus]) { syncStatusTable.insertStatuses(statuses) } @@ -42,7 +49,19 @@ public final class SyncDatabase { public func deleteSelectedForProcessing(_ articleIDs: [String]) { syncStatusTable.deleteSelectedForProcessing(articleIDs) } - + + // MARK: - Suspend and Resume (for iOS) + + /// Close the database and stop running database calls. + /// Any pending calls will complete first. + public func suspend() { + queue.suspend() + } + + /// Open the database and allow for running database calls again. + public func resume() { + queue.resume() + } } // MARK: - Private diff --git a/Frameworks/SyncDatabase/SyncStatusTable.swift b/Frameworks/SyncDatabase/SyncStatusTable.swift index 63a2036fe..0b5c0a44d 100644 --- a/Frameworks/SyncDatabase/SyncStatusTable.swift +++ b/Frameworks/SyncDatabase/SyncStatusTable.swift @@ -10,21 +10,19 @@ import Foundation import Articles import RSDatabase -final class SyncStatusTable: DatabaseTable { - +struct SyncStatusTable: DatabaseTable { + let name = DatabaseTableName.syncStatus - private let queue: RSDatabaseQueue - - init(queue: RSDatabaseQueue) { + private let queue: DatabaseQueue + + init(queue: DatabaseQueue) { self.queue = queue } func selectForProcessing() -> [SyncStatus] { - var statuses: Set? = nil - self.queue.updateSync { database in - + queue.runInDatabaseSync { database in let updateSQL = "update syncStatus set selected = true" database.executeUpdate(updateSQL, withArgumentsIn: nil) @@ -32,32 +30,26 @@ final class SyncStatusTable: DatabaseTable { if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) { statuses = resultSet.mapToSet(self.statusWithRow) } - } return statuses != nil ? Array(statuses!) : [SyncStatus]() - } func selectPendingCount() -> Int { - var count: Int = 0 - self.queue.fetchSync { (database) in + queue.runInDatabaseSync { database in let sql = "select count(*) from syncStatus" if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) { - resultSet.next() - count = Int(resultSet.int(forColumnIndex: 0)) + count = numberWithCountResultSet(resultSet) } - } return count - } func resetSelectedForProcessing(_ articleIDs: [String]) { - self.queue.update { database in + queue.runInTransaction { database in let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" @@ -66,7 +58,7 @@ final class SyncStatusTable: DatabaseTable { } func deleteSelectedForProcessing(_ articleIDs: [String]) { - self.queue.update { database in + queue.runInTransaction { database in let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let deleteSQL = "delete from syncStatus where articleID in \(placeholders)" @@ -75,18 +67,16 @@ final class SyncStatusTable: DatabaseTable { } func insertStatuses(_ statuses: [SyncStatus]) { - self.queue.update { database in + queue.runInTransaction { database in let statusArray = statuses.map { $0.databaseDictionary() } self.insertRows(statusArray, insertType: .orReplace, in: database) } } - } private extension SyncStatusTable { func statusWithRow(_ row: FMResultSet) -> SyncStatus? { - guard let articleID = row.string(forColumn: DatabaseKey.articleID), let rawKey = row.string(forColumn: DatabaseKey.key), let key = ArticleStatus.Key(rawValue: rawKey) else { @@ -97,6 +87,5 @@ private extension SyncStatusTable { let selected = row.bool(forColumn: DatabaseKey.selected) return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected) - } } From 8daf80e8d19601235a4d0715af1c22e7c8a1fd9d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 21:55:11 -0800 Subject: [PATCH 4/9] Update to latest RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index ce9eed0b0..573bf1d99 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit ce9eed0b051d439f429e33a324cffec6dedbf74d +Subproject commit 573bf1d99418b218d264105df620163832c36c34 From 8ee169d9152a34b61563be82ded0f949f6dc2932 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 22:22:04 -0800 Subject: [PATCH 5/9] Update to latest RSDatabase. --- submodules/RSDatabase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 573bf1d99..b2ad9272e 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 573bf1d99418b218d264105df620163832c36c34 +Subproject commit b2ad9272e8003b0aba3c8c961239663ac501f00b From b5f1c69f8eed3e2bc36297b77068397bcb32b3ad Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 22:57:14 -0800 Subject: [PATCH 6/9] Add AccountManager.resumeAll, Account.resume. Add suspend() and resume() to AccountDelegate and to individual AccountDelegate instances. --- Frameworks/Account/Account.swift | 9 ++++++++- Frameworks/Account/AccountDelegate.swift | 5 ++++- Frameworks/Account/AccountManager.swift | 6 +++++- .../FeedWrangler/FeedWranglerAccountDelegate.swift | 12 ++++++++++++ .../Account/Feedbin/FeedbinAccountDelegate.swift | 13 ++++++++++++- .../Account/Feedly/FeedlyAccountDelegate.swift | 12 ++++++++++++ .../Account/LocalAccount/LocalAccountDelegate.swift | 10 ++++++++++ .../ReaderAPI/ReaderAPIAccountDelegate.swift | 13 ++++++++++++- 8 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index a8f99784c..d787bfa51 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -407,9 +407,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func suspend() { delegate.cancelAll(for: self) + delegate.suspend() + database.suspend() save() } - + + public func resume() { + database.resume() + delegate.resume() + } + public func save() { metadataFile.save() webFeedMetadataFile.save() diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 3480a51cb..9ddecdbb7 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -50,5 +50,8 @@ protocol AccountDelegate { func accountWillBeDeleted(_ account: Account) static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result) -> Void) - + + // For iOS, so we can suspend and resume properly. + func suspend() // Make sure no SQLite databases are open. + func resume() } diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index a6483c939..f5e718adb 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -166,7 +166,11 @@ public final class AccountManager: UnreadCountProvider { public func suspendAll() { accounts.forEach { $0.suspend() } } - + + public func resumeAll() { + accounts.forEach { $0.resume() } + } + public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) { let group = DispatchGroup() diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift index f1b4352f4..13f868e81 100644 --- a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift +++ b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift @@ -438,6 +438,18 @@ final class FeedWranglerAccountDelegate: AccountDelegate { } } } + + // MARK: Suspend and Resume (for iOS) + + /// Suspend the sync database so that it can close its SQLite file. + func suspend() { + database.suspend() + } + + /// Resume the sync database — let it reopen its SQLite file. + func resume() { + database.resume() + } } // MARK: Private diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index efe734e1a..cc1dfa709 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -555,7 +555,18 @@ final class FeedbinAccountDelegate: AccountDelegate { } } - + + // MARK: Suspend and Resume (for iOS) + + /// Suspend the sync database so that it can close its SQLite file. + func suspend() { + database.suspend() + } + + /// Resume the sync database — let it reopen its SQLite file. + func resume() { + database.resume() + } } // MARK: Private diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 9a8ce30c3..48db9fa41 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -510,4 +510,16 @@ final class FeedlyAccountDelegate: AccountDelegate { assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.") completion(.success(credentials)) } + + // MARK: Suspend and Resume (for iOS) + + /// Suspend the sync database so that it can close its SQLite file. + func suspend() { + database.suspend() + } + + /// Resume the sync database — let it reopen its SQLite file. + func resume() { + database.resume() + } } diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 23d9dba61..21b1bbbf8 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -202,4 +202,14 @@ final class LocalAccountDelegate: AccountDelegate { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result) -> Void) { return completion(.success(nil)) } + + // MARK: Suspend and Resume (for iOS) + + func suspend() { + // Nothing to do + } + + func resume() { + // Nothing to do + } } diff --git a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 862a30a82..493a29e2c 100644 --- a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -436,7 +436,18 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } } - + + // MARK: Suspend and Resume (for iOS) + + /// Suspend the sync database so that it can close its SQLite file. + func suspend() { + database.suspend() + } + + /// Resume the sync database — let it reopen its SQLite file. + func resume() { + database.resume() + } } // MARK: Private From ed5db537f8fb383cbdd410382126ec75264be924 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 23:04:29 -0800 Subject: [PATCH 7/9] Fix warning in in-progress code. --- .../Account/FeedWrangler/FeedWranglerAccountDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift index 13f868e81..ee2e34ddf 100644 --- a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift +++ b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift @@ -542,7 +542,7 @@ private extension FeedWranglerAccountDelegate { } func syncArticleState(_ account: Account, key: ArticleStatus.Key, flag: Bool, serverFeedItems: [FeedWranglerFeedItem]) { - let serverFeedItemIDs = serverFeedItems.map { String($0.feedID) } + let _ /*serverFeedItemIDs*/ = serverFeedItems.map { String($0.feedID) } // todo generalize this logic } From cd019d6d89f039b056ca02156718f9c025f3306e Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 23:30:13 -0800 Subject: [PATCH 8/9] Update to latest RSCore. --- submodules/RSCore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSCore b/submodules/RSCore index ba7bbb2ce..4a909600c 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit ba7bbb2ce10ee04a730c0a1e425a1b2e9d338520 +Subproject commit 4a909600c568b1773785bd9e18075da92d916c69 From 45804a3d1ad3dbb30402468a352b669cc4cdf636 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 29 Nov 2019 23:42:11 -0800 Subject: [PATCH 9/9] Use the new VoidCompletionBlock instead of (() -> ()). --- Frameworks/Account/Account.swift | 2 +- Frameworks/ArticlesDatabase/ArticlesDatabase.swift | 3 ++- Frameworks/ArticlesDatabase/ArticlesTable.swift | 4 ++-- Frameworks/SyncDatabase/SyncDatabase.swift | 7 ++++--- Frameworks/SyncDatabase/SyncStatusTable.swift | 13 +++++++------ 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index d787bfa51..8f52a0d13 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -732,7 +732,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return updatedArticles } - func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { + func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) { guard !articleIDs.isEmpty else { completionHandler?() return diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 948deefa2..12fbffcba 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -7,6 +7,7 @@ // import Foundation +import RSCore import RSDatabase import RSParser import Articles @@ -147,7 +148,7 @@ public final class ArticlesDatabase { articlesTable.update(webFeedIDsAndItems, defaultRead, completion) } - public func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { + public func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) { articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completionHandler: completionHandler) } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 59e65f4ad..6c505ace3 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -298,13 +298,13 @@ final class ArticlesTable: DatabaseTable { } } - func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { + func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) { queue.runInTransaction { (database) in let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) let statuses = Set(statusesDictionary.values) self.statusesTable.mark(statuses, statusKey, flag, database) if let handler = completionHandler { - DispatchQueue.main.async(execute: handler) + callVoidCompletionBlock(handler) } } } diff --git a/Frameworks/SyncDatabase/SyncDatabase.swift b/Frameworks/SyncDatabase/SyncDatabase.swift index 568da3353..8ffad570b 100644 --- a/Frameworks/SyncDatabase/SyncDatabase.swift +++ b/Frameworks/SyncDatabase/SyncDatabase.swift @@ -7,6 +7,7 @@ // import Foundation +import RSCore import RSDatabase public struct SyncDatabase { @@ -30,7 +31,7 @@ public struct SyncDatabase { // MARK: - API - public func insertStatuses(_ statuses: [SyncStatus], completionHandler: (() -> ())? = nil) { + public func insertStatuses(_ statuses: [SyncStatus], completionHandler: VoidCompletionBlock? = nil) { syncStatusTable.insertStatuses(statuses, completionHandler: completionHandler) } @@ -42,11 +43,11 @@ public struct SyncDatabase { return syncStatusTable.selectPendingCount() } - public func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { + public func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) { syncStatusTable.resetSelectedForProcessing(articleIDs, completionHandler: completionHandler) } - public func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { + public func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) { syncStatusTable.deleteSelectedForProcessing(articleIDs, completionHandler: completionHandler) } diff --git a/Frameworks/SyncDatabase/SyncStatusTable.swift b/Frameworks/SyncDatabase/SyncStatusTable.swift index 23b55c2d5..55361205f 100644 --- a/Frameworks/SyncDatabase/SyncStatusTable.swift +++ b/Frameworks/SyncDatabase/SyncStatusTable.swift @@ -7,6 +7,7 @@ // import Foundation +import RSCore import Articles import RSDatabase @@ -48,36 +49,36 @@ struct SyncStatusTable: DatabaseTable { return count } - func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { + func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) { queue.runInTransaction { database in let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" database.executeUpdate(updateSQL, withArgumentsIn: parameters) if let handler = completionHandler { - DispatchQueue.main.async(execute: handler) + callVoidCompletionBlock(handler) } } } - func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { + func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) { queue.runInTransaction { database in let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let deleteSQL = "delete from syncStatus where articleID in \(placeholders)" database.executeUpdate(deleteSQL, withArgumentsIn: parameters) if let handler = completionHandler { - DispatchQueue.main.async(execute: handler) + callVoidCompletionBlock(handler) } } } - func insertStatuses(_ statuses: [SyncStatus], completionHandler: (() -> ())? = nil) { + func insertStatuses(_ statuses: [SyncStatus], completionHandler: VoidCompletionBlock? = nil) { queue.runInTransaction { database in let statusArray = statuses.map { $0.databaseDictionary() } self.insertRows(statusArray, insertType: .orReplace, in: database) if let handler = completionHandler { - DispatchQueue.main.async(execute: handler) + callVoidCompletionBlock(handler) } } }