Add database change notification
This commit is contained in:
@@ -16,8 +16,8 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/angd-dev/data-lite-core.git", from: "1.0.0"),
|
.package(url: "https://github.com/angd-dev/data-lite-core.git", .upToNextMinor(from: "1.1.0")),
|
||||||
.package(url: "https://github.com/angd-dev/data-lite-coder.git", from: "1.0.0"),
|
.package(url: "https://github.com/angd-dev/data-lite-coder.git", .upToNextMinor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
|
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import Foundation
|
|||||||
import DataLiteCore
|
import DataLiteCore
|
||||||
import DataLiteC
|
import DataLiteC
|
||||||
|
|
||||||
/// A base database service handling transactions and event notifications.
|
/// A base database service that handles transactions and posts change notifications.
|
||||||
///
|
///
|
||||||
/// ## Overview
|
/// ## Overview
|
||||||
///
|
///
|
||||||
/// `DatabaseService` provides a foundational layer for performing transactional database operations
|
/// `DatabaseService` provides a lightweight transactional layer for performing database operations
|
||||||
/// within a thread-safe execution context. It automatically posts lifecycle notifications — such as
|
/// within a thread-safe execution context. It automatically detects modifications to the database
|
||||||
/// commit, rollback, and content changes — allowing observers to react to database updates in real
|
/// and posts a ``databaseDidChange`` notification database updates, allowing observers to react to
|
||||||
/// time. By default, it routes events through ``Foundation/NotificationCenter/database`` so that
|
/// updates.
|
||||||
/// clients can subscribe via a dedicated channel. This service is designed to be subclassed by
|
///
|
||||||
/// higher-level data managers that encapsulate domain logic while relying on consistent connection
|
/// This class is intended to be subclassed by higher-level data managers that encapsulate domain
|
||||||
/// and transaction handling.
|
/// logic while relying on consistent connection and transaction handling.
|
||||||
///
|
///
|
||||||
/// ## Usage
|
/// ## Usage
|
||||||
///
|
///
|
||||||
@@ -57,9 +57,6 @@ import DataLiteC
|
|||||||
/// ### Notifications
|
/// ### Notifications
|
||||||
///
|
///
|
||||||
/// - ``databaseDidChange``
|
/// - ``databaseDidChange``
|
||||||
/// - ``databaseWillCommit``
|
|
||||||
/// - ``databaseDidRollback``
|
|
||||||
/// - ``databaseDidPerform``
|
|
||||||
open class DatabaseService:
|
open class DatabaseService:
|
||||||
ConnectionService,
|
ConnectionService,
|
||||||
DatabaseServiceProtocol,
|
DatabaseServiceProtocol,
|
||||||
@@ -70,47 +67,26 @@ open class DatabaseService:
|
|||||||
|
|
||||||
private let center: NotificationCenter
|
private let center: NotificationCenter
|
||||||
|
|
||||||
/// Notification posted after the database content changes.
|
/// Notification posted after the database content changes with this service.
|
||||||
///
|
|
||||||
/// Observers listen to this event to refresh cached data or update dependent components once
|
|
||||||
/// modifications are committed. The notification’s `userInfo` may include
|
|
||||||
/// ``Foundation/Notification/UserInfoKey/action`` describing the SQLite action.
|
|
||||||
public static let databaseDidChange = Notification.Name("DatabaseService.databaseDidChange")
|
public static let databaseDidChange = Notification.Name("DatabaseService.databaseDidChange")
|
||||||
|
|
||||||
/// Notification posted immediately before a transaction commits.
|
|
||||||
///
|
|
||||||
/// Observers can perform validation or prepare for an upcoming state change while the
|
|
||||||
/// transaction is still in progress.
|
|
||||||
public static let databaseWillCommit = Notification.Name("DatabaseService.databaseWillCommit")
|
|
||||||
|
|
||||||
/// Notification posted after a transaction rolls back.
|
|
||||||
///
|
|
||||||
/// Observers use this event to revert in-memory state or reset caches that rely on pending
|
|
||||||
/// changes.
|
|
||||||
public static let databaseDidRollback = Notification.Name("DatabaseService.databaseDidRollback")
|
|
||||||
|
|
||||||
/// Notification posted after any database operation completes, regardless of outcome.
|
|
||||||
///
|
|
||||||
/// The service emits this event after finishing a `perform(_:)` block so observers can
|
|
||||||
/// synchronize state even when the operation is read-only or aborted.
|
|
||||||
///
|
|
||||||
/// - Important: Confirm that the associated transaction was not rolled back before relying on
|
|
||||||
/// side effects.
|
|
||||||
public static let databaseDidPerform = Notification.Name("DatabaseService.databaseDidPerform")
|
|
||||||
|
|
||||||
// MARK: - Inits
|
// MARK: - Inits
|
||||||
|
|
||||||
/// Creates a database service that posts lifecycle events to the provided notification center.
|
/// Creates a database service with a specified notification center.
|
||||||
///
|
///
|
||||||
/// The underlying connection handling matches ``ConnectionService``; the connection is created
|
/// Configures an internal serial queue for thread-safe access to the database. The connection
|
||||||
/// lazily and all work executes on the managed serial queue.
|
/// 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:
|
/// - Parameters:
|
||||||
/// - provider: A closure that returns a new database connection.
|
/// - provider: A closure that returns a new database connection.
|
||||||
/// - config: An optional configuration closure called after the connection is established and
|
/// - config: An optional configuration closure called after the connection is established and
|
||||||
/// the encryption key is applied.
|
/// the encryption key is applied.
|
||||||
/// - queue: An optional target queue for the internal serial queue.
|
/// - queue: An optional target queue for the internal one.
|
||||||
/// - center: A notification center for posting database events.
|
/// - center: The notification center used to post database change notifications.
|
||||||
public init(
|
public init(
|
||||||
provider: @escaping ConnectionProvider,
|
provider: @escaping ConnectionProvider,
|
||||||
config: ConnectionConfig? = nil,
|
config: ConnectionConfig? = nil,
|
||||||
@@ -121,39 +97,54 @@ open class DatabaseService:
|
|||||||
super.init(provider: provider, config: config, queue: queue)
|
super.init(provider: provider, config: config, queue: queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a database service that posts lifecycle events to the shared database notification
|
/// Creates a database service using the default database notification center.
|
||||||
/// center.
|
|
||||||
///
|
///
|
||||||
/// The connection is established lazily on first access and all work executes on the internal
|
/// Configures an internal serial queue for thread-safe access to the database. The connection
|
||||||
/// queue defined in ``ConnectionService``.
|
/// itself is not created during initialization — it is established lazily on first use (for
|
||||||
|
/// example, inside ``perform(_:)``).
|
||||||
|
///
|
||||||
|
/// The service posts change notifications through ``Foundation/NotificationCenter/databaseCenter``,
|
||||||
|
/// which provides a shared channel for observing database events across the application.
|
||||||
|
///
|
||||||
|
/// The internal queue is created with QoS `.utility`. If `queue` is provided, it becomes the
|
||||||
|
/// target of the internal queue.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - provider: A closure that returns a new database connection.
|
/// - provider: A closure that returns a new database connection.
|
||||||
/// - config: An optional configuration closure called after the connection is established and
|
/// - config: An optional configuration closure called after the connection is established and
|
||||||
/// the encryption key is applied.
|
/// the encryption key is applied.
|
||||||
/// - queue: An optional target queue for the internal serial queue.
|
/// - queue: An optional target queue for the internal one.
|
||||||
public required init(
|
public required init(
|
||||||
provider: @escaping ConnectionProvider,
|
provider: @escaping ConnectionProvider,
|
||||||
config: ConnectionConfig? = nil,
|
config: ConnectionConfig? = nil,
|
||||||
queue: DispatchQueue? = nil
|
queue: DispatchQueue? = nil
|
||||||
) {
|
) {
|
||||||
self.center = .database
|
self.center = .databaseCenter
|
||||||
super.init(provider: provider, config: config, queue: queue)
|
super.init(provider: provider, config: config, queue: queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Performing Operations
|
// MARK: - Performing Operations
|
||||||
|
|
||||||
/// Executes a closure with a managed database connection and posts a completion notification.
|
/// Executes a closure within the context of a managed database connection.
|
||||||
///
|
///
|
||||||
/// The override mirrors ``ConnectionService/perform(_:)`` for queue-confined execution while
|
/// Runs the operation on the service’s internal queue and ensures that the connection is valid
|
||||||
/// ensuring ``DatabaseService/databaseDidPerform`` is delivered after the closure completes.
|
/// before use. If the connection is unavailable or fails during execution, this method throws
|
||||||
|
/// an error.
|
||||||
///
|
///
|
||||||
/// - Parameter closure: The operation to execute using the open connection.
|
/// After the closure completes, if the database content has changed, the service posts a
|
||||||
/// - Returns: The value returned by the closure.
|
/// ``databaseDidChange`` notification through its configured notification center.
|
||||||
/// - Throws: Errors thrown by the closure or underlying connection.
|
///
|
||||||
|
/// - 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 override func perform<T>(_ closure: Perform<T>) throws -> T {
|
public override func perform<T>(_ closure: Perform<T>) throws -> T {
|
||||||
try super.perform { connection in
|
try super.perform { connection in
|
||||||
defer { center.post(name: Self.databaseDidPerform, object: self) }
|
let changes = connection.totalChanges
|
||||||
|
defer {
|
||||||
|
if changes != connection.totalChanges {
|
||||||
|
center.post(name: Self.databaseDidChange, object: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
return try closure(connection)
|
return try closure(connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,28 +212,43 @@ open class DatabaseService:
|
|||||||
|
|
||||||
// MARK: - ConnectionDelegate
|
// MARK: - ConnectionDelegate
|
||||||
|
|
||||||
/// Posts ``DatabaseService/databaseDidChange`` when the database content updates.
|
/// Handles database updates reported by the active connection.
|
||||||
|
///
|
||||||
|
/// Called after an SQL statement modifies the database content. Subclasses can override this
|
||||||
|
/// method to observe specific actions (for example, inserts, updates, or deletes).
|
||||||
|
///
|
||||||
|
/// - Important: This method must not execute SQL statements or otherwise alter the connection
|
||||||
|
/// state.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - connection: The connection that performed the change.
|
/// - connection: The connection that performed the update.
|
||||||
/// - action: The SQLite action describing the modification.
|
/// - action: The SQLite action describing the change.
|
||||||
public func connection(_ connection: any ConnectionProtocol, didUpdate action: SQLiteAction) {
|
open func connection(_ connection: any ConnectionProtocol, didUpdate action: SQLiteAction) {
|
||||||
let userInfo = [Notification.UserInfoKey.action: action]
|
|
||||||
center.post(name: Self.databaseDidChange, object: self, userInfo: userInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Posts ``DatabaseService/databaseWillCommit`` before a transaction commits.
|
/// Called immediately before the connection commits a transaction.
|
||||||
|
///
|
||||||
|
/// Subclasses can override this method to perform validation or consistency checks prior to
|
||||||
|
/// committing. Throwing an error cancels the commit and triggers a rollback.
|
||||||
|
///
|
||||||
|
/// - Important: This method must not execute SQL statements or otherwise alter the connection
|
||||||
|
/// state.
|
||||||
///
|
///
|
||||||
/// - Parameter connection: The connection preparing to commit.
|
/// - Parameter connection: The connection preparing to commit.
|
||||||
public func connectionWillCommit(_ connection: any ConnectionProtocol) throws {
|
/// - Throws: An error to cancel the commit and roll back the transaction.
|
||||||
center.post(name: Self.databaseWillCommit, object: self)
|
open func connectionWillCommit(_ connection: any ConnectionProtocol) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Posts ``DatabaseService/databaseDidRollback`` after a transaction rollback.
|
/// Called after the connection rolls back a transaction.
|
||||||
///
|
///
|
||||||
/// - Parameter connection: The connection that rolled back.
|
/// Subclasses can override this method to handle cleanup or recovery logic following a
|
||||||
public func connectionDidRollback(_ connection: any ConnectionProtocol) {
|
/// rollback.
|
||||||
center.post(name: Self.databaseDidRollback, object: self)
|
///
|
||||||
|
/// - Important: This method must not execute SQL statements or otherwise alter the connection
|
||||||
|
/// state.
|
||||||
|
///
|
||||||
|
/// - Parameter connection: The connection that rolled back the transaction.
|
||||||
|
open func connectionDidRollback(_ connection: any ConnectionProtocol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Internal Methods
|
// MARK: - Internal Methods
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ open class ModelDatabaseService: DatabaseService, @unchecked Sendable {
|
|||||||
/// - provider: A closure that returns a new database connection.
|
/// - provider: A closure that returns a new database connection.
|
||||||
/// - config: Optional configuration for the connection.
|
/// - config: Optional configuration for the connection.
|
||||||
/// - queue: The dispatch queue used for serializing database operations.
|
/// - queue: The dispatch queue used for serializing database operations.
|
||||||
/// - center: The notification center used for database events. Defaults to `.database`.
|
/// - center: The notification center used for database events. Defaults to `.databaseCenter`.
|
||||||
/// - encoder: The encoder for converting models into SQLite rows.
|
/// - encoder: The encoder for converting models into SQLite rows.
|
||||||
/// - decoder: The decoder for converting rows back into model instances.
|
/// - decoder: The decoder for converting rows back into model instances.
|
||||||
public init(
|
public init(
|
||||||
provider: @escaping ConnectionProvider,
|
provider: @escaping ConnectionProvider,
|
||||||
config: ConnectionConfig? = nil,
|
config: ConnectionConfig? = nil,
|
||||||
queue: DispatchQueue? = nil,
|
queue: DispatchQueue? = nil,
|
||||||
center: NotificationCenter = .database,
|
center: NotificationCenter = .databaseCenter,
|
||||||
encoder: RowEncoder,
|
encoder: RowEncoder,
|
||||||
decoder: RowDecoder
|
decoder: RowDecoder
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import DataLiteCore
|
|||||||
/// stores version data using the SQLite `PRAGMA user_version` mechanism. This approach is simple,
|
/// stores version data using the SQLite `PRAGMA user_version` mechanism. This approach is simple,
|
||||||
/// efficient, and requires no additional tables.
|
/// efficient, and requires no additional tables.
|
||||||
///
|
///
|
||||||
/// The generic ``Version`` type must conform to both ``VersionRepresentable`` and
|
/// The generic `Version` type must conform to both ``VersionRepresentable`` and `RawRepresentable`,
|
||||||
/// `RawRepresentable`, with `RawValue == UInt32`. This enables conversion between stored integer
|
/// with `RawValue == UInt32`. This enables conversion between stored integer
|
||||||
/// values and the application’s semantic version type.
|
/// values and the application’s semantic version type.
|
||||||
///
|
///
|
||||||
/// ## Topics
|
/// ## Topics
|
||||||
@@ -26,10 +26,9 @@ import DataLiteCore
|
|||||||
public final class UserVersionStorage<
|
public final class UserVersionStorage<
|
||||||
Version: VersionRepresentable & RawRepresentable
|
Version: VersionRepresentable & RawRepresentable
|
||||||
>: Sendable, VersionStorage where Version.RawValue == UInt32 {
|
>: Sendable, VersionStorage where Version.RawValue == UInt32 {
|
||||||
|
|
||||||
/// Errors related to reading or decoding the stored version.
|
/// Errors related to reading or decoding the stored version.
|
||||||
public enum Error: Swift.Error {
|
public enum Error: Swift.Error {
|
||||||
/// The stored `user_version` value could not be decoded into a valid ``Version``.
|
/// The stored `user_version` value could not be decoded into a valid `Version`.
|
||||||
///
|
///
|
||||||
/// - Parameter value: The invalid raw `UInt32` value.
|
/// - Parameter value: The invalid raw `UInt32` value.
|
||||||
case invalidStoredVersion(UInt32)
|
case invalidStoredVersion(UInt32)
|
||||||
@@ -44,7 +43,7 @@ public final class UserVersionStorage<
|
|||||||
|
|
||||||
/// Returns the current schema version stored in the `user_version` field.
|
/// Returns the current schema version stored in the `user_version` field.
|
||||||
///
|
///
|
||||||
/// Reads the `PRAGMA user_version` value and attempts to decode it into a valid ``Version``.
|
/// Reads the `PRAGMA user_version` value and attempts to decode it into a valid `Version`.
|
||||||
/// If decoding fails, this method throws an error.
|
/// If decoding fails, this method throws an error.
|
||||||
///
|
///
|
||||||
/// - Parameter connection: The active database connection.
|
/// - Parameter connection: The active database connection.
|
||||||
@@ -62,7 +61,7 @@ public final class UserVersionStorage<
|
|||||||
/// Stores the specified schema version in the `user_version` field.
|
/// Stores the specified schema version in the `user_version` field.
|
||||||
///
|
///
|
||||||
/// Updates the SQLite `PRAGMA user_version` value with the raw `UInt32` representation of the
|
/// Updates the SQLite `PRAGMA user_version` value with the raw `UInt32` representation of the
|
||||||
/// provided ``Version``.
|
/// provided `Version`.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - connection: The active database connection.
|
/// - connection: The active database connection.
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
extension Notification {
|
|
||||||
/// A strongly typed key used to access values in a notification’s user info dictionary.
|
|
||||||
///
|
|
||||||
/// ## Overview
|
|
||||||
///
|
|
||||||
/// `UserInfoKey` provides type safety when working with `Notification.userInfo`, replacing raw
|
|
||||||
/// string literals with well-defined constants. This helps prevent typos and improves
|
|
||||||
/// discoverability in database- or system-related notifications.
|
|
||||||
///
|
|
||||||
/// ## Topics
|
|
||||||
///
|
|
||||||
/// ### Keys
|
|
||||||
///
|
|
||||||
/// - ``action``
|
|
||||||
public struct UserInfoKey: RawRepresentable, Hashable, Sendable {
|
|
||||||
/// The raw string value of the key.
|
|
||||||
public let rawValue: String
|
|
||||||
|
|
||||||
/// The key used to store the action associated with a notification.
|
|
||||||
public static let action = Self(rawValue: "action")
|
|
||||||
|
|
||||||
/// Creates a user info key from the provided raw string value.
|
|
||||||
///
|
|
||||||
/// Returns `nil` if the raw value is invalid.
|
|
||||||
///
|
|
||||||
/// - Parameter rawValue: The raw string value of the key.
|
|
||||||
public init?(rawValue: String) {
|
|
||||||
self.rawValue = rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,5 @@ public extension NotificationCenter {
|
|||||||
///
|
///
|
||||||
/// Use this instance to post and observe notifications related to database lifecycle and
|
/// Use this instance to post and observe notifications related to database lifecycle and
|
||||||
/// operations instead of using the shared `NotificationCenter.default`.
|
/// operations instead of using the shared `NotificationCenter.default`.
|
||||||
static let database = NotificationCenter()
|
static let databaseCenter = NotificationCenter()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user