Refactoring
This commit is contained in:
204
Sources/DataRaft/Classes/ConnectionService.swift
Normal file
204
Sources/DataRaft/Classes/ConnectionService.swift
Normal file
@@ -0,0 +1,204 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
/// A base service responsible for establishing and maintaining a database connection.
|
||||
///
|
||||
/// ## Overview
|
||||
///
|
||||
/// `ConnectionService` provides a managed execution environment for database operations. It handles
|
||||
/// connection creation, configuration, encryption key application, and optional reconnection based
|
||||
/// on the associated key provider’s policy.
|
||||
///
|
||||
/// This class guarantees thread-safe access by executing all operations within a dedicated dispatch
|
||||
/// queue. Subclasses may extend it with additional behaviors, such as transaction management or
|
||||
/// lifecycle event posting.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a Service
|
||||
///
|
||||
/// - ``ConnectionProvider``
|
||||
/// - ``ConnectionConfig``
|
||||
/// - ``init(provider:config:queue:)``
|
||||
/// - ``init(connection:config:queue:)``
|
||||
///
|
||||
/// ### Key Management
|
||||
///
|
||||
/// - ``ConnectionServiceKeyProvider``
|
||||
/// - ``keyProvider``
|
||||
///
|
||||
/// ### Connection Lifecycle
|
||||
///
|
||||
/// - ``setNeedsReconnect()``
|
||||
///
|
||||
/// ### Performing Operations
|
||||
///
|
||||
/// - ``ConnectionServiceProtocol/Perform``
|
||||
/// - ``perform(_:)``
|
||||
open class ConnectionService:
|
||||
ConnectionServiceProtocol,
|
||||
@unchecked Sendable
|
||||
{
|
||||
// MARK: - Typealiases
|
||||
|
||||
/// A closure that creates a new database connection.
|
||||
///
|
||||
/// Used for deferred connection creation. Encapsulates initialization logic, configuration, and
|
||||
/// error handling when opening the database.
|
||||
///
|
||||
/// - Returns: An initialized connection instance.
|
||||
/// - Throws: An error if the connection cannot be created or configured.
|
||||
public typealias ConnectionProvider = () throws -> ConnectionProtocol
|
||||
|
||||
/// A closure that configures a newly created connection.
|
||||
///
|
||||
/// Called after the connection is established and, if applicable, after the encryption key has
|
||||
/// been applied. Use this closure to set PRAGMA options or perform additional initialization
|
||||
/// logic.
|
||||
///
|
||||
/// - Parameter connection: The newly created connection to configure.
|
||||
/// - Throws: An error if configuration fails.
|
||||
public typealias ConnectionConfig = (ConnectionProtocol) throws -> Void
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let provider: ConnectionProvider
|
||||
private let config: ConnectionConfig?
|
||||
private let queue: DispatchQueue
|
||||
private let queueKey = DispatchSpecificKey<Void>()
|
||||
|
||||
private var shouldReconnect: Bool {
|
||||
keyProvider?.connectionService(shouldReconnect: self) ?? false
|
||||
}
|
||||
|
||||
private var needsReconnect: Bool = false
|
||||
private var cachedConnection: ConnectionProtocol?
|
||||
|
||||
private var connection: ConnectionProtocol {
|
||||
get throws {
|
||||
guard let cachedConnection, !needsReconnect else {
|
||||
let connection = try connect()
|
||||
cachedConnection = connection
|
||||
needsReconnect = false
|
||||
return connection
|
||||
}
|
||||
return cachedConnection
|
||||
}
|
||||
}
|
||||
|
||||
/// The provider responsible for supplying encryption keys to the service.
|
||||
///
|
||||
/// The key provider may determine whether reconnection is allowed and supply
|
||||
/// the encryption key when the connection is established or restored.
|
||||
public weak var keyProvider: ConnectionServiceKeyProvider?
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
/// Creates a new connection service.
|
||||
///
|
||||
/// Configures an internal serial queue for thread-safe access to the database. The connection
|
||||
/// itself is not created during initialization — it is established lazily on first use (for
|
||||
/// example, inside ``perform(_:)``).
|
||||
///
|
||||
/// The internal queue is created with QoS `.utility`. If `queue` is provided, it becomes the
|
||||
/// target of the internal queue.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - provider: A closure that returns a new database connection.
|
||||
/// - config: An optional configuration closure called after the connection is established and
|
||||
/// the encryption key is applied.
|
||||
/// - queue: An optional target queue for the internal one.
|
||||
public required init(
|
||||
provider: @escaping ConnectionProvider,
|
||||
config: ConnectionConfig? = nil,
|
||||
queue: DispatchQueue? = nil
|
||||
) {
|
||||
self.provider = provider
|
||||
self.config = config
|
||||
self.queue = .init(for: Self.self, qos: .utility)
|
||||
self.queue.setSpecific(key: queueKey, value: ())
|
||||
if let queue = queue {
|
||||
self.queue.setTarget(queue: queue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new connection service using an autoclosure-based provider.
|
||||
///
|
||||
/// This initializer provides a convenient way to wrap an existing connection expression in an
|
||||
/// autoclosure. The connection itself is not created during initialization — it is established
|
||||
/// lazily on first use.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - provider: An autoclosure that returns a new database connection.
|
||||
/// - config: An optional configuration closure called after the connection is established and
|
||||
/// the encryption key is applied.
|
||||
/// - queue: An optional target queue for the internal one.
|
||||
public required convenience init(
|
||||
connection provider: @escaping @autoclosure ConnectionProvider,
|
||||
config: ConnectionConfig? = nil,
|
||||
queue: DispatchQueue? = nil
|
||||
) {
|
||||
self.init(provider: provider, config: config, queue: queue)
|
||||
}
|
||||
|
||||
// MARK: - Connection Lifecycle
|
||||
|
||||
/// Marks the service as requiring reconnection before the next operation.
|
||||
///
|
||||
/// The reconnection behavior depends on the key provider’s implementation of
|
||||
/// ``ConnectionServiceKeyProvider/connectionService(shouldReconnect:)``. If reconnection is
|
||||
/// allowed, the next access to the connection will create and configure a new one.
|
||||
///
|
||||
/// - Returns: `true` if the reconnection flag was set; otherwise, `false`.
|
||||
@discardableResult
|
||||
public func setNeedsReconnect() -> Bool {
|
||||
switch DispatchQueue.getSpecific(key: queueKey) {
|
||||
case .none:
|
||||
return queue.sync { setNeedsReconnect() }
|
||||
case .some:
|
||||
guard shouldReconnect else { return false }
|
||||
needsReconnect = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performing Operations
|
||||
|
||||
/// Executes a closure within the context of a managed database connection.
|
||||
///
|
||||
/// Runs the operation on the service’s internal queue and ensures that the connection is valid
|
||||
/// before use. If the connection is unavailable or fails during execution, this method throws
|
||||
/// an error.
|
||||
///
|
||||
/// - Parameter closure: The operation to perform using the connection.
|
||||
/// - Returns: The result produced by the closure.
|
||||
/// - Throws: An error thrown by the closure or the connection.
|
||||
public func perform<T>(_ closure: Perform<T>) throws -> T {
|
||||
switch DispatchQueue.getSpecific(key: queueKey) {
|
||||
case .none: try queue.sync { try closure(connection) }
|
||||
case .some: try closure(connection)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal Methods
|
||||
|
||||
func connect() throws -> ConnectionProtocol {
|
||||
let connection = try provider()
|
||||
try applyKey(to: connection)
|
||||
try config?(connection)
|
||||
return connection
|
||||
}
|
||||
|
||||
func applyKey(to connection: ConnectionProtocol) throws {
|
||||
guard let keyProvider = keyProvider else { return }
|
||||
do {
|
||||
let key = try keyProvider.connectionService(keyFor: self)
|
||||
let sql = "SELECT count(*) FROM sqlite_master"
|
||||
try connection.apply(key, name: nil)
|
||||
try connection.execute(sql: sql)
|
||||
} catch {
|
||||
keyProvider.connectionService(self, didReceive: error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user