#if canImport(Combine) import Combine import Foundation /// A publisher that delivers values to its downstream subscriber on a /// specific scheduler. /// /// Unlike Combine's Publishers.ReceiveOn, ReceiveValuesOn only re-schedule /// values and completion. It does not re-schedule subscription. /// /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure fileprivate let upstream: Upstream fileprivate let context: Context fileprivate let options: Context.SchedulerOptions? func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { let subscription = ReceiveValuesOnSubscription( upstream: upstream, context: context, options: options, downstream: subscriber) subscriber.receive(subscription: subscription) } } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, Context: Scheduler, Downstream: Subscriber, Upstream.Failure == Downstream.Failure, Upstream.Output == Downstream.Input { private struct Target { let context: Context let options: Context.SchedulerOptions? let downstream: Downstream } private enum State { case waitingForRequest(Upstream, Target) case waitingForSubscription(Target, Subscribers.Demand) case subscribed(Target, Subscription) case finished } private var state: State private let lock = NSRecursiveLock() init( upstream: Upstream, context: Context, options: Context.SchedulerOptions?, downstream: Downstream) { let target = Target(context: context, options: options, downstream: downstream) self.state = .waitingForRequest(upstream, target) } // MARK: Subscription func request(_ demand: Subscribers.Demand) { lock.synchronized { sideEffect in switch state { case let .waitingForRequest(upstream, target): state = .waitingForSubscription(target, demand) sideEffect = { upstream.receive(subscriber: self) } case let .waitingForSubscription(target, currentDemand): state = .waitingForSubscription(target, demand + currentDemand) case let .subscribed(_, subcription): sideEffect = { subcription.request(demand) } case .finished: break } } } func cancel() { lock.synchronized { sideEffect in switch state { case .waitingForRequest, .waitingForSubscription: state = .finished case let .subscribed(_, subcription): state = .finished sideEffect = { subcription.cancel() } case .finished: break } } } // MARK: Subscriber func receive(subscription: Subscription) { lock.synchronized { sideEffect in switch state { case let .waitingForSubscription(target, currentDemand): state = .subscribed(target, subscription) sideEffect = { subscription.request(currentDemand) } case .waitingForRequest, .subscribed: preconditionFailure() case .finished: // We receive the upstream subscription requested by // `upstream.receive(subscriber: self)` above. // // But self has been cancelled since, so let's cancel this // upstream subscription that has turned purposeless. // // This cancellation avoids the bug described in // https://github.com/groue/GRDB.swift/pull/932 // TODO: write a regression test. sideEffect = { subscription.cancel() } } } } func receive(_ input: Upstream.Output) -> Subscribers.Demand { lock.synchronized { sideEffect in switch state { case let .subscribed(target, _): sideEffect = { target.context.schedule(options: target.options) { self._receive(input) } } case .waitingForRequest, .waitingForSubscription, .finished: break } } // TODO: what problem are we creating by returning .unlimited and // ignoring downstream's result? // // `Publisher.receive(on:options:)` does not document its behavior // regarding backpressure. return .unlimited } func receive(completion: Subscribers.Completion) { lock.synchronized { sideEffect in switch state { case .waitingForRequest, .waitingForSubscription: break case let .subscribed(target, _): sideEffect = { target.context.schedule(options: target.options) { self._receive(completion: completion) } } case .finished: break } } } private func _receive(_ input: Upstream.Output) { lock.synchronized { sideEffect in switch state { case .waitingForRequest, .waitingForSubscription: break case let .subscribed(target, _): // TODO: don't ignore demand sideEffect = { _ = target.downstream.receive(input) } case .finished: break } } } private func _receive(completion: Subscribers.Completion) { lock.synchronized { sideEffect in switch state { case .waitingForRequest, .waitingForSubscription: break case let .subscribed(target, _): state = .finished sideEffect = { target.downstream.receive(completion: completion) } case .finished: break } } } } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// /// The difference with the stock `receive(on:options:)` Combine method is /// that only values and completion are re-scheduled. Subscriptions are not. func receiveValues(on scheduler: S, options: S.SchedulerOptions? = nil) -> ReceiveValuesOn { ReceiveValuesOn(upstream: self, context: scheduler, options: options) } } #endif