This commit is contained in:
Maurice Parker 2019-12-01 12:04:33 -06:00
commit c65ccb0504
17 changed files with 185 additions and 81 deletions

View File

@ -407,9 +407,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func suspend() { public func suspend() {
delegate.cancelAll(for: self) delegate.cancelAll(for: self)
delegate.suspend()
database.suspend()
save() save()
} }
public func resume() {
database.resume()
delegate.resume()
}
public func save() { public func save() {
metadataFile.save() metadataFile.save()
webFeedMetadataFile.save() webFeedMetadataFile.save()
@ -725,7 +732,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return updatedArticles return updatedArticles
} }
func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) {
guard !articleIDs.isEmpty else { guard !articleIDs.isEmpty else {
completionHandler?() completionHandler?()
return return

View File

@ -74,6 +74,7 @@
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */; }; 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */; };
844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844B2980210CE3BF004020B3 /* RSWeb.framework */; }; 844B2981210CE3BF004020B3 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844B2980210CE3BF004020B3 /* RSWeb.framework */; };
8469F81C1F6DD15E0084783E /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935101F62486800CEBD24 /* Account.swift */; }; 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 */; }; 846E77451F6EF9B900A165E2 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419740D1F6DD25F006346C4 /* Container.swift */; };
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742C1F6DDE84006346C4 /* LocalAccountDelegate.swift */; }; 846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742C1F6DDE84006346C4 /* LocalAccountDelegate.swift */; };
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; }; 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; };
@ -292,6 +293,7 @@
844B297C2106C7EC004020B3 /* WebFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebFeed.swift; sourceTree = "<group>"; }; 844B297C2106C7EC004020B3 /* WebFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebFeed.swift; sourceTree = "<group>"; };
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = "<group>"; }; 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = "<group>"; };
844B2980210CE3BF004020B3 /* RSWeb.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSWeb.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 846E77531F6F00E300A165E2 /* AccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
848934F61F62484F00CEBD24 /* Account.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Account.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 848934FA1F62484F00CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };

View File

@ -50,5 +50,8 @@ protocol AccountDelegate {
func accountWillBeDeleted(_ account: Account) func accountWillBeDeleted(_ account: Account)
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void)
// For iOS, so we can suspend and resume properly.
func suspend() // Make sure no SQLite databases are open.
func resume()
} }

View File

@ -166,7 +166,11 @@ public final class AccountManager: UnreadCountProvider {
public func suspendAll() { public func suspendAll() {
accounts.forEach { $0.suspend() } accounts.forEach { $0.suspend() }
} }
public func resumeAll() {
accounts.forEach { $0.resume() }
}
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) { public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) {
let group = DispatchGroup() let group = DispatchGroup()

View File

@ -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 // MARK: Private
@ -530,7 +542,7 @@ private extension FeedWranglerAccountDelegate {
} }
func syncArticleState(_ account: Account, key: ArticleStatus.Key, flag: Bool, serverFeedItems: [FeedWranglerFeedItem]) { 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 // todo generalize this logic
} }

View File

@ -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 // MARK: Private

View File

@ -510,4 +510,16 @@ final class FeedlyAccountDelegate: AccountDelegate {
assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.") assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.")
completion(.success(credentials)) 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()
}
} }

View File

@ -202,4 +202,14 @@ final class LocalAccountDelegate: AccountDelegate {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil)) return completion(.success(nil))
} }
// MARK: Suspend and Resume (for iOS)
func suspend() {
// Nothing to do
}
func resume() {
// Nothing to do
}
} }

View File

@ -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 // MARK: Private

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import RSCore
import RSDatabase import RSDatabase
import RSParser import RSParser
import Articles import Articles
@ -22,21 +23,29 @@ public typealias UpdateArticlesCompletionBlock = (Set<Article>?, Set<Article>?)
public final class ArticlesDatabase { 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 articlesTable: ArticlesTable
private let queue: DatabaseQueue
public init(databaseFilePath: String, accountID: String) { 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) self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue)
queue.createTables(usingStatements: ArticlesDatabase.tableCreationStatements) queue.runCreateStatements(ArticlesDatabase.tableCreationStatements)
queue.update { (database) in queue.runInDatabase { database in
if !self.articlesTable.containsColumn("searchRowID", in: database) { if !self.articlesTable.containsColumn("searchRowID", in: database) {
database.executeStatements("ALTER TABLE articles add column searchRowID INTEGER;") database.executeStatements("ALTER TABLE articles add column searchRowID INTEGER;")
} }
database.executeStatements("CREATE INDEX if not EXISTS articles_searchRowID on articles(searchRowID);") 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;") 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 { DispatchQueue.main.async {
self.articlesTable.indexUnindexedArticles() self.articlesTable.indexUnindexedArticles()
} }
@ -139,7 +148,7 @@ public final class ArticlesDatabase {
articlesTable.update(webFeedIDsAndItems, defaultRead, completion) articlesTable.update(webFeedIDsAndItems, defaultRead, completion)
} }
public func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { public func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) {
articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completionHandler: completionHandler) articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completionHandler: completionHandler)
} }
@ -161,6 +170,19 @@ public final class ArticlesDatabase {
return articlesTable.mark(articles, statusKey, flag) 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 // MARK: - Caches
/// Call to free up some memory. Should be done when the app is backgrounded, for instance. /// Call to free up some memory. Should be done when the app is backgrounded, for instance.

View File

@ -16,7 +16,7 @@ final class ArticlesTable: DatabaseTable {
let name: String let name: String
private let accountID: String private let accountID: String
private let queue: RSDatabaseQueue private let queue: DatabaseQueue
private let statusesTable: StatusesTable private let statusesTable: StatusesTable
private let authorsLookupTable: DatabaseLookupTable private let authorsLookupTable: DatabaseLookupTable
private let attachmentsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable
@ -32,7 +32,7 @@ final class ArticlesTable: DatabaseTable {
private typealias ArticlesFetchMethod = (FMDatabase) -> Set<Article> private typealias ArticlesFetchMethod = (FMDatabase) -> Set<Article>
init(name: String, accountID: String, queue: RSDatabaseQueue) { init(name: String, accountID: String, queue: DatabaseQueue) {
self.name = name self.name = name
self.accountID = accountID self.accountID = accountID
@ -168,7 +168,7 @@ final class ArticlesTable: DatabaseTable {
func fetchArticlesMatching(_ searchString: String) -> Set<Article> { func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
var articles: Set<Article> = Set<Article>() var articles: Set<Article> = Set<Article>()
queue.fetchSync { (database) in queue.runInDatabaseSync { (database) in
articles = self.fetchArticlesMatching(searchString, database) articles = self.fetchArticlesMatching(searchString, database)
} }
return articles return articles
@ -255,7 +255,7 @@ final class ArticlesTable: DatabaseTable {
articleIDs.formUnion(parsedItems.articleIDs()) articleIDs.formUnion(parsedItems.articleIDs())
} }
self.queue.update { (database) in self.queue.runInTransaction { (database) in
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count) assert(statusesDictionary.count == articleIDs.count)
@ -298,13 +298,13 @@ final class ArticlesTable: DatabaseTable {
} }
} }
func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: (() -> ())? = nil) { func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completionHandler: VoidCompletionBlock? = nil) {
self.queue.update { (database) in queue.runInTransaction { (database) in
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database)
let statuses = Set(statusesDictionary.values) let statuses = Set(statusesDictionary.values)
self.statusesTable.mark(statuses, statusKey, flag, database) self.statusesTable.mark(statuses, statusKey, flag, database)
if let handler = completionHandler { if let handler = completionHandler {
DispatchQueue.main.async(execute: handler) callVoidCompletionBlock(handler)
} }
} }
} }
@ -319,7 +319,7 @@ final class ArticlesTable: DatabaseTable {
var unreadCountDictionary = UnreadCountDictionary() var unreadCountDictionary = UnreadCountDictionary()
queue.fetch { (database) in queue.runInDatabase { (database) in
for webFeedID in webFeedIDs { for webFeedID in webFeedIDs {
unreadCountDictionary[webFeedID] = self.fetchUnreadCount(webFeedID, database) unreadCountDictionary[webFeedID] = self.fetchUnreadCount(webFeedID, database)
} }
@ -338,7 +338,7 @@ final class ArticlesTable: DatabaseTable {
return return
} }
queue.fetch { (database) in queue.runInDatabase { (database) in
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.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;" 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;"
@ -360,7 +360,7 @@ final class ArticlesTable: DatabaseTable {
let cutoffDate = articleCutoffDate 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;" 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 { guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
@ -390,7 +390,7 @@ final class ArticlesTable: DatabaseTable {
return return
} }
queue.fetch { (database) in queue.runInDatabase { (database) in
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;" 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(webFeedIDs) as [Any] let parameters = Array(webFeedIDs) as [Any]
@ -419,7 +419,7 @@ final class ArticlesTable: DatabaseTable {
func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? { func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? {
var statuses: Set<ArticleStatus>? var statuses: Set<ArticleStatus>?
self.queue.updateSync { (database) in self.queue.runInTransactionSync { (database) in
statuses = self.statusesTable.mark(articles.statuses(), statusKey, flag, database) statuses = self.statusesTable.mark(articles.statuses(), statusKey, flag, database)
} }
return statuses return statuses
@ -428,7 +428,7 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Indexing // MARK: - Indexing
func indexUnindexedArticles() { func indexUnindexedArticles() {
queue.fetch { (database) in queue.runInDatabase { (database) in
let sql = "select articleID from articles where searchRowID is null limit 500;" let sql = "select articleID from articles where searchRowID is null limit 500;"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
return return
@ -448,7 +448,7 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Caches // MARK: - Caches
func emptyCaches() { func emptyCaches() {
queue.run { _ in queue.runInDatabase { _ in
self.databaseArticlesCache = [String: DatabaseArticle]() self.databaseArticlesCache = [String: DatabaseArticle]()
} }
} }
@ -462,7 +462,7 @@ final class ArticlesTable: DatabaseTable {
if webFeedIDs.isEmpty { if webFeedIDs.isEmpty {
return return
} }
queue.run { (database) in queue.runInDatabase { (database) in
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
let sql = "select articleID from articles where feedID not in \(placeholders);" let sql = "select articleID from articles where feedID not in \(placeholders);"
let parameters = Array(webFeedIDs) as [Any] let parameters = Array(webFeedIDs) as [Any]
@ -487,14 +487,14 @@ private extension ArticlesTable {
private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set<Article> { private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set<Article> {
var articles = Set<Article>() var articles = Set<Article>()
queue.fetchSync { (database) in queue.runInDatabaseSync { (database) in
articles = fetchMethod(database) articles = fetchMethod(database)
} }
return articles return articles
} }
private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ callback: @escaping ArticleSetBlock) { private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ callback: @escaping ArticleSetBlock) {
queue.fetch { (database) in queue.runInDatabase { (database) in
let articles = fetchMethod(database) let articles = fetchMethod(database)
DispatchQueue.main.async { DispatchQueue.main.async {
callback(articles) callback(articles)

View File

@ -61,10 +61,10 @@ final class ArticleSearchInfo: Hashable {
final class SearchTable: DatabaseTable { final class SearchTable: DatabaseTable {
let name = "search" let name = "search"
private let queue: RSDatabaseQueue private let queue: DatabaseQueue
private weak var articlesTable: ArticlesTable? private weak var articlesTable: ArticlesTable?
init(queue: RSDatabaseQueue, articlesTable: ArticlesTable) { init(queue: DatabaseQueue, articlesTable: ArticlesTable) {
self.queue = queue self.queue = queue
self.articlesTable = articlesTable self.articlesTable = articlesTable
} }
@ -73,7 +73,7 @@ final class SearchTable: DatabaseTable {
if articleIDs.isEmpty { if articleIDs.isEmpty {
return return
} }
queue.update { (database) in queue.runInTransaction { (database) in
self.ensureIndexedArticles(articleIDs, database) self.ensureIndexedArticles(articleIDs, database)
} }
} }

View File

@ -19,9 +19,9 @@ final class StatusesTable: DatabaseTable {
let name = DatabaseTableName.statuses let name = DatabaseTableName.statuses
private let cache = StatusCache() private let cache = StatusCache()
private let queue: RSDatabaseQueue private let queue: DatabaseQueue
init(queue: RSDatabaseQueue) { init(queue: DatabaseQueue) {
self.queue = queue self.queue = queue
} }
@ -88,7 +88,7 @@ final class StatusesTable: DatabaseTable {
func fetchArticleIDs(_ sql: String) -> Set<String> { func fetchArticleIDs(_ sql: String) -> Set<String> {
var articleIDs = Set<String>() var articleIDs = Set<String>()
queue.fetchSync { (database) in queue.runInDatabaseSync { (database) in
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
return return
} }

View File

@ -7,23 +7,31 @@
// //
import Foundation import Foundation
import RSCore
import RSDatabase import RSDatabase
public final class SyncDatabase { public struct SyncDatabase {
private let syncStatusTable: SyncStatusTable
public init(databaseFilePath: String) {
let queue = RSDatabaseQueue(filepath: databaseFilePath, excludeFromBackup: false)
self.syncStatusTable = SyncStatusTable(queue: queue)
queue.createTables(usingStatementsSync: SyncDatabase.tableCreationStatements)
queue.vacuumIfNeeded()
/// When SyncDatabase is suspended, database calls will crash the app.
public var isSuspended: Bool {
return queue.isSuspended
} }
public func insertStatuses(_ statuses: [SyncStatus], completionHandler: (() -> ())? = nil) { 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], completionHandler: VoidCompletionBlock? = nil) {
syncStatusTable.insertStatuses(statuses, completionHandler: completionHandler) syncStatusTable.insertStatuses(statuses, completionHandler: completionHandler)
} }
@ -35,14 +43,26 @@ public final class SyncDatabase {
return syncStatusTable.selectPendingCount() return syncStatusTable.selectPendingCount()
} }
public func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { public func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) {
syncStatusTable.resetSelectedForProcessing(articleIDs, completionHandler: completionHandler) 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) syncStatusTable.deleteSelectedForProcessing(articleIDs, completionHandler: completionHandler)
} }
// 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 // MARK: - Private

View File

@ -7,24 +7,23 @@
// //
import Foundation import Foundation
import RSCore
import Articles import Articles
import RSDatabase import RSDatabase
final class SyncStatusTable: DatabaseTable { struct SyncStatusTable: DatabaseTable {
let name = DatabaseTableName.syncStatus let name = DatabaseTableName.syncStatus
private let queue: RSDatabaseQueue private let queue: DatabaseQueue
init(queue: RSDatabaseQueue) { init(queue: DatabaseQueue) {
self.queue = queue self.queue = queue
} }
func selectForProcessing() -> [SyncStatus] { func selectForProcessing() -> [SyncStatus] {
var statuses: Set<SyncStatus>? = nil var statuses: Set<SyncStatus>? = nil
self.queue.updateSync { database in queue.runInDatabaseSync { database in
let updateSQL = "update syncStatus set selected = true" let updateSQL = "update syncStatus set selected = true"
database.executeUpdate(updateSQL, withArgumentsIn: nil) database.executeUpdate(updateSQL, withArgumentsIn: nil)
@ -32,70 +31,62 @@ final class SyncStatusTable: DatabaseTable {
if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) { if let resultSet = database.executeQuery(selectSQL, withArgumentsIn: nil) {
statuses = resultSet.mapToSet(self.statusWithRow) statuses = resultSet.mapToSet(self.statusWithRow)
} }
} }
return statuses != nil ? Array(statuses!) : [SyncStatus]() return statuses != nil ? Array(statuses!) : [SyncStatus]()
} }
func selectPendingCount() -> Int { func selectPendingCount() -> Int {
var count: Int = 0 var count: Int = 0
self.queue.fetchSync { (database) in queue.runInDatabaseSync { database in
let sql = "select count(*) from syncStatus" let sql = "select count(*) from syncStatus"
if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) { if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) {
resultSet.next() count = numberWithCountResultSet(resultSet)
count = Int(resultSet.int(forColumnIndex: 0))
} }
} }
return count return count
} }
func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { func resetSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) {
self.queue.update { database in queue.runInTransaction { database in
let parameters = articleIDs.map { $0 as AnyObject } let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)" let updateSQL = "update syncStatus set selected = false where articleID in \(placeholders)"
database.executeUpdate(updateSQL, withArgumentsIn: parameters) database.executeUpdate(updateSQL, withArgumentsIn: parameters)
if let handler = completionHandler { if let handler = completionHandler {
DispatchQueue.main.async(execute: handler) callVoidCompletionBlock(handler)
} }
} }
} }
func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: (() -> ())? = nil) { func deleteSelectedForProcessing(_ articleIDs: [String], completionHandler: VoidCompletionBlock? = nil) {
self.queue.update { database in queue.runInTransaction { database in
let parameters = articleIDs.map { $0 as AnyObject } let parameters = articleIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
let deleteSQL = "delete from syncStatus where articleID in \(placeholders)" let deleteSQL = "delete from syncStatus where articleID in \(placeholders)"
database.executeUpdate(deleteSQL, withArgumentsIn: parameters) database.executeUpdate(deleteSQL, withArgumentsIn: parameters)
if let handler = completionHandler { if let handler = completionHandler {
DispatchQueue.main.async(execute: handler) callVoidCompletionBlock(handler)
} }
} }
} }
func insertStatuses(_ statuses: [SyncStatus], completionHandler: (() -> ())? = nil) { func insertStatuses(_ statuses: [SyncStatus], completionHandler: VoidCompletionBlock? = nil) {
self.queue.update { database in queue.runInTransaction { database in
let statusArray = statuses.map { $0.databaseDictionary() } let statusArray = statuses.map { $0.databaseDictionary() }
self.insertRows(statusArray, insertType: .orReplace, in: database) self.insertRows(statusArray, insertType: .orReplace, in: database)
if let handler = completionHandler { if let handler = completionHandler {
DispatchQueue.main.async(execute: handler) callVoidCompletionBlock(handler)
} }
} }
} }
} }
private extension SyncStatusTable { 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 = ArticleStatus.Key(rawValue: rawKey) else {
@ -106,6 +97,5 @@ private extension SyncStatusTable {
let selected = row.bool(forColumn: DatabaseKey.selected) let selected = row.bool(forColumn: DatabaseKey.selected)
return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected) return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected)
} }
} }

@ -1 +1 @@
Subproject commit ba7bbb2ce10ee04a730c0a1e425a1b2e9d338520 Subproject commit 4a909600c568b1773785bd9e18075da92d916c69

@ -1 +1 @@
Subproject commit ce9eed0b051d439f429e33a324cffec6dedbf74d Subproject commit b2ad9272e8003b0aba3c8c961239663ac501f00b