942 lines
36 KiB
Swift
942 lines
36 KiB
Swift
#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) })
|
||
}
|
||
}
|