add swiftUI code
This commit is contained in:
@@ -0,0 +1,941 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#endif
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
public struct ValueObservation<Reducer: _ValueReducer> {
|
||||
var events = ValueObservationEvents()
|
||||
|
||||
/// A boolean value indicating whether the observation requires write access
|
||||
/// when it fetches fresh values.
|
||||
///
|
||||
/// The `requiresWriteAccess` property is false by default. When true, a
|
||||
/// `ValueObservation` has a write access to the database, and its fetches
|
||||
/// are automatically wrapped in a savepoint:
|
||||
///
|
||||
/// ```swift
|
||||
/// var observation = ValueObservation.tracking { db in
|
||||
/// // write access allowed
|
||||
/// ...
|
||||
/// }
|
||||
/// observation.requiresWriteAccess = true
|
||||
/// ```
|
||||
///
|
||||
/// Setting the `requiresWriteAccess` flag can disable scheduling
|
||||
/// optimizations when the observation is started in a ``DatabasePool``.
|
||||
public var requiresWriteAccess = false
|
||||
|
||||
var trackingMode: ValueObservationTrackingMode
|
||||
|
||||
/// The reducer is created when observation starts, and is triggered upon
|
||||
/// each database change.
|
||||
var makeReducer: () -> Reducer
|
||||
|
||||
/// Returns a ValueObservation with a transformed reducer.
|
||||
func mapReducer<R>(_ transform: @escaping (Reducer) -> R) -> ValueObservation<R> {
|
||||
let makeReducer = self.makeReducer
|
||||
return ValueObservation<R>(
|
||||
events: events,
|
||||
requiresWriteAccess: requiresWriteAccess,
|
||||
trackingMode: trackingMode,
|
||||
makeReducer: { transform(makeReducer()) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the tracked region
|
||||
enum ValueObservationTrackingMode {
|
||||
/// The tracked region is constant and explicit.
|
||||
///
|
||||
/// Use case:
|
||||
///
|
||||
/// // Tracked Region is always the full player table
|
||||
/// ValueObservation.trackingConstantRegion(Player.all()) { db in ... }
|
||||
case constantRegion([any DatabaseRegionConvertible])
|
||||
|
||||
/// The tracked region is constant and inferred from the fetched values.
|
||||
///
|
||||
/// Use case:
|
||||
///
|
||||
/// // Tracked Region is always the full player table
|
||||
/// ValueObservation.trackingConstantRegion { db in Player.fetchAll(db) }
|
||||
case constantRegionRecordedFromSelection
|
||||
|
||||
/// The tracked region is not constant, and inferred from the fetched values.
|
||||
///
|
||||
/// Use case:
|
||||
///
|
||||
/// // Tracked Region is the one row of the table, and it changes on
|
||||
/// // each fetch.
|
||||
/// ValueObservation.tracking { db in
|
||||
/// try Player.fetchOne(db, id: Int.random(in: 1.1000))
|
||||
/// }
|
||||
case nonConstantRegionRecordedFromSelection
|
||||
}
|
||||
|
||||
struct ValueObservationEvents: Refinable {
|
||||
var willStart: (() -> Void)?
|
||||
var willTrackRegion: ((DatabaseRegion) -> Void)?
|
||||
var databaseDidChange: (() -> Void)?
|
||||
var didFail: ((Error) -> Void)?
|
||||
var didCancel: (() -> Void)?
|
||||
}
|
||||
|
||||
typealias ValueObservationStart<T> = (
|
||||
_ onError: @escaping (Error) -> Void,
|
||||
_ onChange: @escaping (T) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
|
||||
extension ValueObservation: Refinable {
|
||||
|
||||
// MARK: - Starting Observation
|
||||
|
||||
/// Starts observing the database.
|
||||
///
|
||||
/// The observation lasts until the returned cancellable is cancelled
|
||||
/// or deallocated.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// let cancellable = try observation.start(in: dbQueue) { error in
|
||||
/// // handle error
|
||||
/// } onChange: { (players: [Player]) in
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// By default, fresh values are dispatched asynchronously on the
|
||||
/// main dispatch queue. You can change this behavior by providing a
|
||||
/// scheduler.
|
||||
///
|
||||
/// For example, the ``ValueObservationScheduler/immediate`` scheduler
|
||||
/// notifies all values on the main dispatch queue, and notifies the first
|
||||
/// one immediately when the observation starts. The `immediate` scheduling
|
||||
/// requires that the observation starts from the main dispatch queue (a
|
||||
/// fatal error is raised otherwise):
|
||||
///
|
||||
/// ```swift
|
||||
/// let cancellable = try observation.start(in: dbQueue, scheduling: .immediate) { error in
|
||||
/// // handle error
|
||||
/// } onChange: { (players: [Player]) in
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// // <- here "Fresh players" is already printed.
|
||||
/// ```
|
||||
///
|
||||
/// - parameter reader: A DatabaseReader.
|
||||
/// - parameter scheduler: A ValueObservationScheduler. By default, fresh
|
||||
/// values are dispatched asynchronously on the main queue.
|
||||
/// - parameter onError: The closure to execute when the observation fails.
|
||||
/// - parameter onChange: The closure to execute on receipt of a
|
||||
/// fresh value.
|
||||
/// - returns: A DatabaseCancellable that can stop the observation.
|
||||
public func start(
|
||||
in reader: some DatabaseReader,
|
||||
scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main),
|
||||
onError: @escaping (Error) -> Void,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
where Reducer: ValueReducer
|
||||
{
|
||||
let observation = self.with {
|
||||
$0.events.didFail = concat($0.events.didFail, onError)
|
||||
}
|
||||
observation.events.willStart?()
|
||||
return reader._add(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
}
|
||||
|
||||
// MARK: - Debugging
|
||||
|
||||
/// Performs the specified closures when observation events occur.
|
||||
///
|
||||
/// All closures run on unspecified dispatch queues: don't make
|
||||
/// any assumption.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - willStart: The closure to execute when the observation starts.
|
||||
/// - willFetch: The closure to execute when the observed value is
|
||||
/// about to be fetched.
|
||||
/// - willTrackRegion: The closure to execute when the observation
|
||||
/// starts tracking a database region.
|
||||
/// - databaseDidChange: The closure to execute after the observation
|
||||
/// was impacted by a database change.
|
||||
/// - didReceiveValue: The closure to execute on fresh values.
|
||||
/// - didFail: The closure to execute when the observation fails.
|
||||
/// - didCancel: The closure to execute when the observation is
|
||||
/// cancelled.
|
||||
/// - returns: A `ValueObservation` that performs the specified closures
|
||||
/// when ValueObservation events occur.
|
||||
public func handleEvents(
|
||||
willStart: (() -> Void)? = nil,
|
||||
willFetch: (() -> Void)? = nil,
|
||||
willTrackRegion: ((DatabaseRegion) -> Void)? = nil,
|
||||
databaseDidChange: (() -> Void)? = nil,
|
||||
didReceiveValue: ((Reducer.Value) -> Void)? = nil,
|
||||
didFail: ((Error) -> Void)? = nil,
|
||||
didCancel: (() -> Void)? = nil)
|
||||
-> ValueObservation<ValueReducers.Trace<Reducer>>
|
||||
{
|
||||
self
|
||||
.mapReducer { reducer in
|
||||
ValueReducers.Trace(
|
||||
base: reducer,
|
||||
// Adding the willFetch handler to the reducer is handy: we
|
||||
// are sure not to miss any fetch.
|
||||
willFetch: willFetch ?? { },
|
||||
// Adding the didReceiveValue handler to the reducer is necessary:
|
||||
// the type of the value may change with the `map` operator.
|
||||
didReceiveValue: didReceiveValue ?? { _ in })
|
||||
}
|
||||
.with {
|
||||
$0.events.willStart = concat($0.events.willStart, willStart)
|
||||
$0.events.willTrackRegion = concat($0.events.willTrackRegion, willTrackRegion)
|
||||
$0.events.databaseDidChange = concat($0.events.databaseDidChange, databaseDidChange)
|
||||
$0.events.didFail = concat($0.events.didFail, didFail)
|
||||
$0.events.didCancel = concat($0.events.didCancel, didCancel)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints log messages for all observation events.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let cancellable = ValueObservation
|
||||
/// .tracking(Player.fetchCount)
|
||||
/// .print("Observe player count")
|
||||
/// .start(in: dbQueue, onError: { _ in }, onChange: { _ in })
|
||||
///
|
||||
/// // Prints:
|
||||
/// // Observe player count: start
|
||||
/// // Observe player count: fetch
|
||||
/// // Observe player count: tracked region: player(*)
|
||||
/// // Observe player count: value: 0
|
||||
/// // Observe player count: database did change
|
||||
/// // Observe player count: fetch
|
||||
/// // Observe player count: value: 1
|
||||
/// ```
|
||||
///
|
||||
/// - parameter prefix: A string —- which defaults to empty -— with which to
|
||||
/// prefix all log messages.
|
||||
/// - parameter stream: A stream for text output that receives messages, and
|
||||
/// which directs output to the console by default. A custom stream can be
|
||||
/// used to log messages to other destinations.
|
||||
public func print(
|
||||
_ prefix: String = "",
|
||||
to stream: TextOutputStream? = nil)
|
||||
-> ValueObservation<ValueReducers.Trace<Reducer>>
|
||||
{
|
||||
let lock = NSLock()
|
||||
let prefix = prefix.isEmpty ? "" : "\(prefix): "
|
||||
var stream = stream ?? PrintOutputStream()
|
||||
return handleEvents(
|
||||
willStart: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)start") },
|
||||
willFetch: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)fetch") },
|
||||
willTrackRegion: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)tracked region: \($0)") },
|
||||
databaseDidChange: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)database did change") },
|
||||
didReceiveValue: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)value: \($0)") },
|
||||
didFail: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)failure: \($0)") },
|
||||
didCancel: {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
stream.write("\(prefix)cancel") })
|
||||
}
|
||||
|
||||
// MARK: - Fetching Values
|
||||
|
||||
/// Fetches the initial value.
|
||||
func fetchInitialValue(_ db: Database) throws -> Reducer.Value
|
||||
where Reducer: ValueReducer
|
||||
{
|
||||
var reducer = makeReducer()
|
||||
guard let value = try reducer._value(reducer._fetch(db)) else {
|
||||
fatalError("Broken contract: reducer has no initial value")
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
extension ValueObservation {
|
||||
// MARK: - Asynchronous Observation
|
||||
/// Returns an asynchronous sequence of observed values.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// for try await players in observation.values(in: dbQueue) {
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter reader: A DatabaseReader.
|
||||
/// - parameter scheduler: A ValueObservationScheduler. By default, fresh
|
||||
/// values are dispatched asynchronously on the main dispatch queue.
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public func values(
|
||||
in reader: some DatabaseReader,
|
||||
scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main),
|
||||
bufferingPolicy: AsyncValueObservation<Reducer.Value>.BufferingPolicy = .unbounded)
|
||||
-> AsyncValueObservation<Reducer.Value>
|
||||
where Reducer: ValueReducer
|
||||
{
|
||||
AsyncValueObservation(bufferingPolicy: bufferingPolicy) { onError, onChange in
|
||||
self.start(in: reader, scheduling: scheduler, onError: onError, onChange: onChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [GRDB7] Make it Sendable for easier integration with AsyncAlgorithms
|
||||
/// An asynchronous sequence of values observed by a ``ValueObservation``.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// An `AsyncValueObservation` sequence produces a fresh value whenever the
|
||||
/// results of database requests change.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// for try await players in observation.values(in: dbQueue) {
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You build an `AsyncValueObservation` from ``ValueObservation`` or
|
||||
/// ``SharedValueObservation``.
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public struct AsyncValueObservation<Element>: AsyncSequence {
|
||||
public typealias BufferingPolicy = AsyncThrowingStream<Element, Error>.Continuation.BufferingPolicy
|
||||
public typealias AsyncIterator = Iterator
|
||||
|
||||
var bufferingPolicy: BufferingPolicy
|
||||
var start: ValueObservationStart<Element>
|
||||
|
||||
public func makeAsyncIterator() -> Iterator {
|
||||
// This cancellable will be retained by the Iterator, which itself will
|
||||
// be retained by the Swift async runtime.
|
||||
//
|
||||
// We must not retain this cancellable in any other way, in order to
|
||||
// cancel the observation when the Swift async runtime releases
|
||||
// the iterator.
|
||||
var cancellable: AnyDatabaseCancellable?
|
||||
let stream = AsyncThrowingStream(Element.self, bufferingPolicy: bufferingPolicy) { continuation in
|
||||
cancellable = start(
|
||||
// onError
|
||||
{ error in
|
||||
continuation.finish(throwing: error)
|
||||
},
|
||||
// onChange
|
||||
{ [weak cancellable] element in
|
||||
if case .terminated = continuation.yield(element) {
|
||||
// TODO: I could never see this code running. Is it needed?
|
||||
cancellable?.cancel()
|
||||
}
|
||||
})
|
||||
continuation.onTermination = { @Sendable [weak cancellable] _ in
|
||||
cancellable?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
let iterator = stream.makeAsyncIterator()
|
||||
if let cancellable {
|
||||
return Iterator(
|
||||
iterator: iterator,
|
||||
cancellable: cancellable)
|
||||
} else {
|
||||
// GRDB bug: there is no point throwing any error.
|
||||
fatalError("Expected AsyncThrowingStream to have started the observation already")
|
||||
}
|
||||
}
|
||||
|
||||
public struct Iterator: AsyncIteratorProtocol {
|
||||
var iterator: AsyncThrowingStream<Element, Error>.AsyncIterator
|
||||
let cancellable: AnyDatabaseCancellable
|
||||
|
||||
public mutating func next() async throws -> Element? {
|
||||
try await iterator.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Combine)
|
||||
extension ValueObservation {
|
||||
// MARK: - Publishing Observed Values
|
||||
|
||||
/// Returns a publisher of observed values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// let publisher = observation.publisher(in: dbQueue)
|
||||
///
|
||||
/// let cancellable = publisher.sink { completion in
|
||||
/// // handle completion
|
||||
/// } receiveValue: { (players: [Player]) in
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// By default, fresh values are dispatched asynchronously on the
|
||||
/// main dispatch queue. You can change this behavior by providing a
|
||||
/// scheduler.
|
||||
///
|
||||
/// For example, the ``ValueObservationScheduler/immediate`` scheduler
|
||||
/// notifies all values on the main dispatch queue, and notifies the first
|
||||
/// one immediately when the observation starts. The `immediate` scheduling
|
||||
/// requires that the observation starts from the main dispatch queue (a
|
||||
/// fatal error is raised otherwise):
|
||||
///
|
||||
/// ```swift
|
||||
/// let publisher = observation.publisher(in: dbQueue, scheduling: .immediate)
|
||||
///
|
||||
/// let cancellable = publisher.sink { completion in
|
||||
/// // handle completion
|
||||
/// } receiveValue: { (players: [Player]) in
|
||||
/// print("Fresh players: \(players)")
|
||||
/// }
|
||||
/// // <- here "Fresh players" is already printed.
|
||||
/// ```
|
||||
///
|
||||
/// - parameter reader: A DatabaseReader.
|
||||
/// - parameter scheduler: A ValueObservationScheduler. By default, fresh
|
||||
/// values are dispatched asynchronously on the main dispatch queue.
|
||||
/// - returns: A Combine publisher
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public func publisher(
|
||||
in reader: some DatabaseReader,
|
||||
scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main))
|
||||
-> DatabasePublishers.Value<Reducer.Value>
|
||||
where Reducer: ValueReducer
|
||||
{
|
||||
DatabasePublishers.Value { (onError, onChange) in
|
||||
self.start(
|
||||
in: reader,
|
||||
scheduling: scheduler,
|
||||
onError: onError,
|
||||
onChange: onChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension DatabasePublishers {
|
||||
/// A publisher that publishes the values of a ``ValueObservation``.
|
||||
///
|
||||
/// You build such a publisher from ``ValueObservation``
|
||||
/// or ``SharedValueObservation``.
|
||||
public struct Value<Output>: Publisher {
|
||||
public typealias Failure = Error
|
||||
private let start: ValueObservationStart<Output>
|
||||
|
||||
init(start: @escaping ValueObservationStart<Output>) {
|
||||
self.start = start
|
||||
}
|
||||
|
||||
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
|
||||
let subscription = ValueSubscription(
|
||||
start: start,
|
||||
downstream: subscriber)
|
||||
subscriber.receive(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private class ValueSubscription<Downstream: Subscriber>: Subscription
|
||||
where Downstream.Failure == Error
|
||||
{
|
||||
private struct WaitingForDemand {
|
||||
let downstream: Downstream
|
||||
let start: ValueObservationStart<Downstream.Input>
|
||||
}
|
||||
|
||||
private struct Observing {
|
||||
let downstream: Downstream
|
||||
var remainingDemand: Subscribers.Demand
|
||||
}
|
||||
|
||||
private enum State {
|
||||
/// Waiting for demand, not observing the database.
|
||||
case waitingForDemand(WaitingForDemand)
|
||||
|
||||
/// Observing the database. Self.observer is not nil.
|
||||
case observing(Observing)
|
||||
|
||||
/// Completed or cancelled, not observing the database.
|
||||
case finished
|
||||
}
|
||||
|
||||
// Cancellable is not stored in self.state because we must enter the
|
||||
// .observing state *before* the observation starts, so that the user
|
||||
// can change the state even before the cancellable is known.
|
||||
private var cancellable: AnyDatabaseCancellable?
|
||||
private var state: State
|
||||
private var lock = NSRecursiveLock() // Allow re-entrancy
|
||||
|
||||
init(
|
||||
start: @escaping ValueObservationStart<Downstream.Input>,
|
||||
downstream: Downstream)
|
||||
{
|
||||
state = .waitingForDemand(WaitingForDemand(
|
||||
downstream: downstream,
|
||||
start: start))
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.synchronized {
|
||||
switch state {
|
||||
case let .waitingForDemand(info):
|
||||
guard demand > 0 else {
|
||||
return
|
||||
}
|
||||
state = .observing(Observing(
|
||||
downstream: info.downstream,
|
||||
remainingDemand: demand))
|
||||
let cancellable = info.start(
|
||||
{ [weak self] error in self?.receiveCompletion(.failure(error)) },
|
||||
{ [weak self] value in self?.receive(value) })
|
||||
|
||||
// State may have been altered (error or cancellation)
|
||||
switch state {
|
||||
case .waitingForDemand:
|
||||
preconditionFailure()
|
||||
case .observing:
|
||||
self.cancellable = cancellable
|
||||
case .finished:
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
case var .observing(info):
|
||||
info.remainingDemand += demand
|
||||
state = .observing(info)
|
||||
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.synchronized { sideEffect in
|
||||
let cancellable = self.cancellable
|
||||
self.cancellable = nil
|
||||
self.state = .finished
|
||||
sideEffect = {
|
||||
cancellable?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func receive(_ value: Downstream.Input) {
|
||||
lock.synchronized {
|
||||
if case let .observing(info) = state,
|
||||
info.remainingDemand > .none
|
||||
{
|
||||
let additionalDemand = info.downstream.receive(value)
|
||||
if case var .observing(info) = state {
|
||||
info.remainingDemand += additionalDemand
|
||||
info.remainingDemand -= 1
|
||||
state = .observing(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func receiveCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||
lock.synchronized { sideEffect in
|
||||
if case let .observing(info) = state {
|
||||
cancellable = nil
|
||||
state = .finished
|
||||
sideEffect = {
|
||||
info.downstream.receive(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension ValueObservation {
|
||||
|
||||
// MARK: - Creating ValueObservation
|
||||
|
||||
/// Creates an optimized `ValueObservation` that notifies the fetched value
|
||||
/// whenever it changes.
|
||||
///
|
||||
/// Unlike observations created with ``tracking(_:)``, the returned
|
||||
/// observation can reduce database contention, by not blocking
|
||||
/// database writes when fresh values are fetched. It can also avoid
|
||||
/// fetching fresh values from the main thread, after the database was
|
||||
/// modified on the main thread.
|
||||
///
|
||||
/// Those scheduling optimizations are only applied when the observation
|
||||
/// is started from a ``DatabasePool``. You can start such an
|
||||
/// observation from a ``DatabaseQueue``, but the optimizations will not
|
||||
/// be applied. The notified values will be the same, though. This makes
|
||||
/// it possible to use a pool in the main application, and an in-memory
|
||||
/// queue in tests and Xcode previews.
|
||||
///
|
||||
/// **Precondition**: The `fetch` function must perform requests that fetch
|
||||
/// from a single and constant database region. This region is made of
|
||||
/// tables, columns, and rowids of individual rows. All changes that happen
|
||||
/// outside of this region are not notified.
|
||||
///
|
||||
/// For example, the observations below track a constant region and can
|
||||
/// be optimized:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Tracks the full 'player' table
|
||||
/// let observation = ValueObservation.trackingConstantRegion { db -> [Player] in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// // Tracks the row with id 42 in the 'player' table
|
||||
/// let observation = ValueObservation.trackingConstantRegion { db -> Player? in
|
||||
/// try Player.fetchOne(db, key: 42)
|
||||
/// }
|
||||
///
|
||||
/// // Tracks the 'score' column in the 'player' table
|
||||
/// let observation = ValueObservation.trackingConstantRegion { db -> Int? in
|
||||
/// try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player")
|
||||
/// }
|
||||
///
|
||||
/// // Tracks both the 'player' and 'team' tables
|
||||
/// let observation = ValueObservation.trackingConstantRegion { db -> ([Team], [Player]) in
|
||||
/// let teams = try Team.fetchAll(db)
|
||||
/// let players = try Player.fetchAll(db)
|
||||
/// return (teams, players)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **Observations that do not track a constant database region must not
|
||||
/// use this method, because some changes may not be notified to
|
||||
/// the application.**
|
||||
///
|
||||
/// For example, the observations below do not track a constant region.
|
||||
/// They are correctly defined with ``tracking(_:)``, since
|
||||
/// `trackingConstantRegion(_:)` is unsuited:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Does not always track the same row in the 'player' table:
|
||||
/// let observation = ValueObservation.tracking { db -> Player in
|
||||
/// let config = try AppConfiguration.find(db)
|
||||
/// let playerId: Int64 = config.favoritePlayerId
|
||||
/// return try Player.find(db, id: playerId)
|
||||
/// }
|
||||
///
|
||||
/// // Does not always track the 'player' table, or not always the same
|
||||
/// // rows in the 'player' table:
|
||||
/// let observation = ValueObservation.tracking { db -> [Player] in
|
||||
/// let config = try AppConfiguration.find(db)
|
||||
/// let playerIds: [Int64] = config.favoritePlayerIds
|
||||
/// // Not only playerIds can change, but when it is empty,
|
||||
/// // the player table is not tracked at all.
|
||||
/// return try Player.fetchAll(db, ids: playerIds)
|
||||
/// }
|
||||
///
|
||||
/// // Sometimes tracks the 'food' table, and sometimes the 'beverage' table.
|
||||
/// let observation = ValueObservation.tracking { db -> Int in
|
||||
/// let config = try AppConfiguration.find(db)
|
||||
/// switch config.selection {
|
||||
/// case .food:
|
||||
/// return try Food.fetchCount(db)
|
||||
/// case .beverage:
|
||||
/// return try Beverage.fetchCount(db)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Since only observations of a constant region can achieve important
|
||||
/// scheduling optimizations (such as the guarantee that fresh values
|
||||
/// are never fetched from the main thread –
|
||||
/// see <doc:ValueObservation#ValueObservation-Scheduling>), you can
|
||||
/// always create one:
|
||||
///
|
||||
/// - With ``tracking(regions:fetch:)``, you provide all tracked
|
||||
/// region(s) when the observation is created:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Optimized observation that explicitly tracks the
|
||||
/// // 'appConfiguration', 'food', and 'beverage' tables:
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [
|
||||
/// AppConfiguration.all(),
|
||||
/// Food.all(),
|
||||
/// Beverage.all(),
|
||||
/// ],
|
||||
/// fetch: { db -> Int in
|
||||
/// let config = try AppConfiguration.find(db)
|
||||
/// switch config.selection {
|
||||
/// case .food:
|
||||
/// return try Food.fetchCount(db)
|
||||
/// case .beverage:
|
||||
/// return try Beverage.fetchCount(db)
|
||||
/// }
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - With ``Database/registerAccess(to:)``, you extend the list of
|
||||
/// tracked region(s) from the fetching closure:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Optimized observation that implicitly tracks the
|
||||
/// // 'appConfiguration' table, and explicitly tracks 'food'
|
||||
/// // and 'beverage':
|
||||
/// let observation = ValueObservation.trackingConstantRegion { db -> Int in
|
||||
/// try db.registerAccess(to: Food.all())
|
||||
/// try db.registerAccess(to: Beverage.all())
|
||||
///
|
||||
/// let config = try AppConfiguration.find(db)
|
||||
/// switch config.selection {
|
||||
/// case .food:
|
||||
/// return try Food.fetchCount(db)
|
||||
/// case .beverage:
|
||||
/// return try Beverage.fetchCount(db)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter fetch: The closure that fetches the observed value.
|
||||
public static func trackingConstantRegion<Value>(
|
||||
_ fetch: @escaping (Database) throws -> Value)
|
||||
-> Self
|
||||
where Reducer == ValueReducers.Fetch<Value>
|
||||
{
|
||||
.init(
|
||||
trackingMode: .constantRegionRecordedFromSelection,
|
||||
makeReducer: { ValueReducers.Fetch(fetch: fetch) })
|
||||
}
|
||||
|
||||
/// Creates a `ValueObservation` that notifies the fetched value whenever
|
||||
/// the provided regions are modified.
|
||||
///
|
||||
/// Only database transactions that impact the provided regions trigger the
|
||||
/// notification of fresh values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Tracks the full database
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: .fullDatabase,
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the full 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: Player.all(),
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the full 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: Table("player"),
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the row with id 42 in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: Player.filter(id: 42),
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the 'score' column in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: Player.select(Column("score"),
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the 'score' column in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: SQLRequest("SELECT score FROM player"),
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks both the 'player' and 'team' tables
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// region: Player.all(), Team.all(),
|
||||
/// fetch: { db in ... })
|
||||
/// ```
|
||||
///
|
||||
/// Unlike observations created with ``tracking(_:)``, the returned
|
||||
/// observation can reduce database contention, by not blocking
|
||||
/// database writes when fresh values are fetched. It can also avoid
|
||||
/// fetching fresh values from the main thread, after the database was
|
||||
/// modified on the main thread.
|
||||
///
|
||||
/// Those scheduling optimizations are only applied when the observation
|
||||
/// is started from a ``DatabasePool``. You can start such an
|
||||
/// observation from a ``DatabaseQueue``, but the optimizations will not
|
||||
/// be applied. The notified values will be the same, though. This makes
|
||||
/// it possible to use a pool in the main application, and an in-memory
|
||||
/// queue in tests and Xcode previews.
|
||||
///
|
||||
/// - parameter region: A region to observe.
|
||||
/// - parameter otherRegions: A list of supplementary regions
|
||||
/// to observe.
|
||||
/// - parameter fetch: The closure that fetches the observed value.
|
||||
public static func tracking<Value>(
|
||||
region: any DatabaseRegionConvertible,
|
||||
_ otherRegions: any DatabaseRegionConvertible...,
|
||||
fetch: @escaping (Database) throws -> Value)
|
||||
-> Self
|
||||
where Reducer == ValueReducers.Fetch<Value>
|
||||
{
|
||||
tracking(regions: [region] + otherRegions, fetch: fetch)
|
||||
}
|
||||
|
||||
/// Creates a `ValueObservation` that notifies the fetched value whenever
|
||||
/// the provided regions are modified.
|
||||
///
|
||||
/// Only database transactions that impact the provided regions trigger the
|
||||
/// notification of fresh values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Tracks the full database
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [.fullDatabase],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the full 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [Player.all()],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the full 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [Table("player")],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the row with id 42 in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [Player.filter(id: 42)],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the 'score' column in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [Player.select(Column("score")],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks the 'score' column in the 'player' table
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [SQLRequest("SELECT score FROM player")],
|
||||
/// fetch: { db in ... })
|
||||
///
|
||||
/// // Tracks both the 'player' and 'team' tables
|
||||
/// let observation = ValueObservation.tracking(
|
||||
/// regions: [Player.all(), Team.all()],
|
||||
/// fetch: { db in ... })
|
||||
/// ```
|
||||
///
|
||||
/// Unlike observations created with ``tracking(_:)``, the returned
|
||||
/// observation can reduce database contention, by not blocking
|
||||
/// database writes when fresh values are fetched. It can also avoid
|
||||
/// fetching fresh values from the main thread, after the database was
|
||||
/// modified on the main thread.
|
||||
///
|
||||
/// Those scheduling optimizations are only applied when the observation
|
||||
/// is started from a ``DatabasePool``. You can start such an
|
||||
/// observation from a ``DatabaseQueue``, but the optimizations will not
|
||||
/// be applied. The notified values will be the same, though. This makes
|
||||
/// it possible to use a pool in the main application, and an in-memory
|
||||
/// queue in tests and Xcode previews.
|
||||
///
|
||||
/// - parameter regions: An array of observed regions.
|
||||
/// - parameter fetch: The closure that fetches the observed value.
|
||||
public static func tracking<Value>(
|
||||
regions: [any DatabaseRegionConvertible],
|
||||
fetch: @escaping (Database) throws -> Value)
|
||||
-> Self
|
||||
where Reducer == ValueReducers.Fetch<Value>
|
||||
{
|
||||
.init(
|
||||
trackingMode: .constantRegion(regions),
|
||||
makeReducer: { ValueReducers.Fetch(fetch: fetch) })
|
||||
}
|
||||
|
||||
/// Creates a `ValueObservation` that notifies the fetched values whenever
|
||||
/// it changes.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Player.fetchAll(db)
|
||||
/// }
|
||||
///
|
||||
/// let cancellable = try observation.start(in: dbQueue) { error in
|
||||
/// // handle error
|
||||
/// } onChange: { (players: [Player]) in
|
||||
/// print("Players have changed")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// An observation can perform multiple requests, from multiple database
|
||||
/// tables, and even use raw SQL:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct HallOfFame {
|
||||
/// var totalPlayerCount: Int
|
||||
/// var bestPlayers: [Player]
|
||||
/// }
|
||||
///
|
||||
/// // An observation of HallOfFame
|
||||
/// let observation = ValueObservation.tracking { db -> HallOfFame in
|
||||
/// let totalPlayerCount = try Player.fetchCount(db)
|
||||
///
|
||||
/// let bestPlayers = try Player
|
||||
/// .order(Column("score").desc)
|
||||
/// .limit(10)
|
||||
/// .fetchAll(db)
|
||||
///
|
||||
/// return HallOfFame(
|
||||
/// totalPlayerCount: totalPlayerCount,
|
||||
/// bestPlayers: bestPlayers)
|
||||
/// }
|
||||
///
|
||||
/// // An observation of the maximum score
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter fetch: The closure that fetches the observed value.
|
||||
public static func tracking<Value>(
|
||||
_ fetch: @escaping (Database) throws -> Value)
|
||||
-> Self
|
||||
where Reducer == ValueReducers.Fetch<Value>
|
||||
{
|
||||
.init(
|
||||
trackingMode: .nonConstantRegionRecordedFromSelection,
|
||||
makeReducer: { ValueReducers.Fetch(fetch: fetch) })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user