mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Move modules to Modules folder.
This commit is contained in:
259
Modules/RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal file
259
Modules/RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal file
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// DatabaseQueue.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 11/13/19.
|
||||
// Copyright © 2019 Brent Simmons. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
import RSDatabaseObjC
|
||||
|
||||
/// Manage a serial queue and a SQLite database.
|
||||
/// It replaces RSDatabaseQueue, which is deprecated.
|
||||
/// Main-thread only.
|
||||
/// Important note: on iOS, the queue can be suspended
|
||||
/// in order to support background refreshing.
|
||||
public final class DatabaseQueue {
|
||||
|
||||
/// Check to see if the queue is suspended. Read-only.
|
||||
/// Calling suspend() and resume() will change the value of this property.
|
||||
/// This will return true only on iOS — on macOS it’s always false.
|
||||
public var isSuspended: Bool {
|
||||
#if os(iOS)
|
||||
precondition(Thread.isMainThread)
|
||||
return _isSuspended
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
private var _isSuspended = true
|
||||
private var isCallingDatabase = false
|
||||
private let database: FMDatabase
|
||||
private let databasePath: String
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
private let targetDispatchQueue: DispatchQueue
|
||||
#if os(iOS)
|
||||
private let databaseLock = NSLock()
|
||||
#endif
|
||||
|
||||
/// When init returns, the database will not be suspended: it will be ready for database calls.
|
||||
public init(databasePath: String) {
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
self.serialDispatchQueue = DispatchQueue(label: "DatabaseQueue (Serial) - \(databasePath)", attributes: .initiallyInactive)
|
||||
self.targetDispatchQueue = DispatchQueue(label: "DatabaseQueue (Target) - \(databasePath)")
|
||||
self.serialDispatchQueue.setTarget(queue: self.targetDispatchQueue)
|
||||
self.serialDispatchQueue.activate()
|
||||
|
||||
self.databasePath = databasePath
|
||||
self.database = FMDatabase(path: databasePath)!
|
||||
openDatabase()
|
||||
_isSuspended = false
|
||||
}
|
||||
|
||||
// MARK: - Suspend and Resume
|
||||
|
||||
/// Close the SQLite database and don’t allow database calls until resumed.
|
||||
/// This is for iOS, where we need to close the SQLite database in some conditions.
|
||||
///
|
||||
/// After calling suspend, if you call into the database before calling resume,
|
||||
/// your code will not run, and runInDatabaseSync and runInTransactionSync will
|
||||
/// both throw DatabaseQueueError.isSuspended.
|
||||
///
|
||||
/// On Mac, suspend() and resume() are no-ops, since there isn’t a need for them.
|
||||
public func suspend() {
|
||||
#if os(iOS)
|
||||
precondition(Thread.isMainThread)
|
||||
guard !_isSuspended else {
|
||||
return
|
||||
}
|
||||
|
||||
_isSuspended = true
|
||||
|
||||
serialDispatchQueue.suspend()
|
||||
targetDispatchQueue.async {
|
||||
self.lockDatabase()
|
||||
self.database.close()
|
||||
self.unlockDatabase()
|
||||
DispatchQueue.main.async {
|
||||
self.serialDispatchQueue.resume()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Open the SQLite database. Allow database calls again.
|
||||
/// This is also for iOS only.
|
||||
public func resume() {
|
||||
#if os(iOS)
|
||||
precondition(Thread.isMainThread)
|
||||
guard _isSuspended else {
|
||||
return
|
||||
}
|
||||
|
||||
serialDispatchQueue.suspend()
|
||||
targetDispatchQueue.sync {
|
||||
if _isSuspended {
|
||||
lockDatabase()
|
||||
openDatabase()
|
||||
unlockDatabase()
|
||||
_isSuspended = false
|
||||
}
|
||||
}
|
||||
serialDispatchQueue.resume()
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Make Database Calls
|
||||
|
||||
/// Run a DatabaseBlock synchronously. This call will block the main thread
|
||||
/// potentially for a while, depending on how long it takes to execute
|
||||
/// the DatabaseBlock *and* depending on how many other calls have been
|
||||
/// scheduled on the queue. Use sparingly — prefer async versions.
|
||||
public func runInDatabaseSync(_ databaseBlock: DatabaseBlock) {
|
||||
precondition(Thread.isMainThread)
|
||||
serialDispatchQueue.sync {
|
||||
self._runInDatabase(self.database, databaseBlock, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a DatabaseBlock asynchronously.
|
||||
public func runInDatabase(_ databaseBlock: @escaping DatabaseBlock) {
|
||||
precondition(Thread.isMainThread)
|
||||
serialDispatchQueue.async {
|
||||
self._runInDatabase(self.database, databaseBlock, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a DatabaseBlock wrapped in a transaction synchronously.
|
||||
/// Transactions help performance significantly when updating the database.
|
||||
/// Nevertheless, it’s best to avoid this because it will block the main thread —
|
||||
/// prefer the async `runInTransaction` instead.
|
||||
public func runInTransactionSync(_ databaseBlock: @escaping DatabaseBlock) {
|
||||
precondition(Thread.isMainThread)
|
||||
serialDispatchQueue.sync {
|
||||
self._runInDatabase(self.database, databaseBlock, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a DatabaseBlock wrapped in a transaction asynchronously.
|
||||
/// Transactions help performance significantly when updating the database.
|
||||
public func runInTransaction(_ databaseBlock: @escaping DatabaseBlock) {
|
||||
precondition(Thread.isMainThread)
|
||||
serialDispatchQueue.async {
|
||||
self._runInDatabase(self.database, databaseBlock, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all the lines that start with "create".
|
||||
/// Use this to create tables, indexes, etc.
|
||||
public func runCreateStatements(_ statements: String) throws {
|
||||
precondition(Thread.isMainThread)
|
||||
var error: DatabaseError? = nil
|
||||
runInDatabaseSync { result in
|
||||
switch result {
|
||||
case .success(let database):
|
||||
statements.enumerateLines { (line, stop) in
|
||||
if line.lowercased().hasPrefix("create") {
|
||||
database.executeStatements(line)
|
||||
}
|
||||
stop = false
|
||||
}
|
||||
case .failure(let databaseError):
|
||||
error = databaseError
|
||||
}
|
||||
}
|
||||
if let error = error {
|
||||
throw(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact the database. This should be done from time to time —
|
||||
/// weekly-ish? — to keep up the performance level of a database.
|
||||
/// Generally a thing to do at startup, if it’s been a while
|
||||
/// since the last vacuum() call. You almost certainly want to call
|
||||
/// vacuumIfNeeded instead.
|
||||
public func vacuum() {
|
||||
precondition(Thread.isMainThread)
|
||||
runInDatabase { result in
|
||||
result.database?.executeStatements("vacuum;")
|
||||
}
|
||||
}
|
||||
|
||||
/// Vacuum the database if it’s been more than `daysBetweenVacuums` since the last vacuum.
|
||||
/// Normally you would call this right after initing a DatabaseQueue.
|
||||
///
|
||||
/// - Returns: true if database will be vacuumed.
|
||||
@discardableResult
|
||||
public func vacuumIfNeeded(daysBetweenVacuums: Int) -> Bool {
|
||||
precondition(Thread.isMainThread)
|
||||
let defaultsKey = "DatabaseQueue-LastVacuumDate-\(databasePath)"
|
||||
let minimumVacuumInterval = TimeInterval(daysBetweenVacuums * (60 * 60 * 24)) // Doesn’t have to be precise
|
||||
let now = Date()
|
||||
let cutoffDate = now - minimumVacuumInterval
|
||||
if let lastVacuumDate = UserDefaults.standard.object(forKey: defaultsKey) as? Date {
|
||||
if lastVacuumDate < cutoffDate {
|
||||
vacuum()
|
||||
UserDefaults.standard.set(now, forKey: defaultsKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Never vacuumed — almost certainly a new database.
|
||||
// Just set the LastVacuumDate pref to now and skip vacuuming.
|
||||
UserDefaults.standard.set(now, forKey: defaultsKey)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private extension DatabaseQueue {
|
||||
|
||||
func lockDatabase() {
|
||||
#if os(iOS)
|
||||
databaseLock.lock()
|
||||
#endif
|
||||
}
|
||||
|
||||
func unlockDatabase() {
|
||||
#if os(iOS)
|
||||
databaseLock.unlock()
|
||||
#endif
|
||||
}
|
||||
|
||||
func _runInDatabase(_ database: FMDatabase, _ databaseBlock: DatabaseBlock, _ useTransaction: Bool) {
|
||||
lockDatabase()
|
||||
defer {
|
||||
unlockDatabase()
|
||||
}
|
||||
|
||||
precondition(!isCallingDatabase)
|
||||
|
||||
isCallingDatabase = true
|
||||
autoreleasepool {
|
||||
if _isSuspended {
|
||||
databaseBlock(.failure(.isSuspended))
|
||||
}
|
||||
else {
|
||||
if useTransaction {
|
||||
database.beginTransaction()
|
||||
}
|
||||
databaseBlock(.success(database))
|
||||
if useTransaction {
|
||||
database.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
isCallingDatabase = false
|
||||
}
|
||||
|
||||
func openDatabase() {
|
||||
database.open()
|
||||
database.executeStatements("PRAGMA synchronous = 1;")
|
||||
database.setShouldCacheStatements(true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user