How do I forward Publisher output to a downstream Subscriber through a custom operator? - swift

I have a potential use for Combine, but I am having a lot of trouble with the implementation details. The goal is to provide an Publisher that will do the following:
Search for a cached value, and emit that value, or:
Refer the subscriber to an upstream publisher that will emit a value, storing it in the appropriate cache location
I understand that this could be done using existing operators, but I would like to learn how to make a custom Operator/Publisher/Subscription pattern, if possible.
I'd like the usage to be similar to the following bit of pseduocode:
URLSession.shared.dataTaskPublisher(for: url)
.cache(with: { someSortOfCachingPolicy })
.sink()
In order to implement this, I am guessing at what Apple does for things like map and flatMap.
I have created a CachePublisher to try to capture the Upstream Publisher:
struct CachePublisher<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
var upstream: Upstream
var getCache: ()->Output?
var setCache: (Output)->Void
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = CachePublisherSubscription(subscriber: subscriber, upstream: upstream, getCache: getCache, setCache: setCache)
subscriber.receive(subscription: subscription)
}
init(_ upstream: Upstream, getCache: #escaping ()->Output?, setCache: #escaping (Output)->Void) {
self.upstream = upstream
self.getCache = getCache
self.setCache = setCache
}
}
This is followed up with a Subscription:
extension CachePublisher {
class CachePublisherSubscription<S: Subscriber>: Subscription where S.Input == Upstream.Output, S.Failure == Upstream.Failure {
var subscriber: S
var upstream: Upstream
var setCache: (Output)->Void
var getCache: ()->Output?
init(subscriber: S, upstream: Upstream, getCache: #escaping ()->Output?, setCache: #escaping (Output)->Void) {
self.subscriber = subscriber
self.upstream = upstream
self.getCache = getCache
self.setCache = setCache
}
func request(_ demand: Subscribers.Demand) {
///check the cache for a value that satisfies the type
///return a value from the upstream publisher if not
if let output = self.getCache() {
subscriber.receive(output)
} else {
//forward an upstream value?
//how? an entire publisher/subscriber chain?
}
}
func cancel() {
}
}
}
And finally, a function so you can pass the upstream publisher to the CachePublisher
extension Publisher {
func cache() -> CachePublisher<Self> {
return CachePublisher(self, getCache: { nil }, setCache: { _ in })
}
}
I have no idea what to put in the required methods, or how to pass the subscriber up the chain to the upstream publisher. Or how to capture values from the upstream publisher.
The idea that came into my head is that downstream subscribers sort of create a nesting doll type structure, but I just don't know how to connect them.

You don't need the whole Publisher/Publishers/Subscription dance, you can customize the subscribe method without needing a custom class. Existing Combine operators to the rescue here :).
extension Publisher {
func cache(read: #escaping Publishers.Cache<Self>.Read,
write: #escaping Publishers.Cache<Self>.Write) -> Publishers.Cache<Self> {
Publishers.Cache(upstream: self, read: read, write: write)
}
}
extension Publishers {
struct Cache<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
typealias Read = () -> Output?
typealias Write = (Output) -> Void
private let upstream: P
private let read: Read
private let write: Write
init(upstream: P, read: #escaping Read, write: #escaping Write) {
self.upstream = upstream
self.read = read
self.write = write
}
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
if let cachedValue = read() {
Just(cachedValue).setFailureType(to: Failure.self).receive(subscriber: subscriber)
} else {
upstream.handleEvents(receiveOutput: write).receive(subscriber: subscriber)
}
}
}
}
handleEvents kinda breaks the "pure functions" paradigm that is recommended to be followed when writing custom operators pipelines, however as you anyway need to write to the cache, and that's already a side effect, the added impact of calling handleEvents is not that big.

Making the custom Subscription also a Subscriber allows it to connect both directions. When the cache getter produces a result, it is sent to the downstream subscriber. However, when the getter does not, the demand is forwarded to the upstream publisher, which emits a value.
That value is then captured by the Subscriber methods of the custom Subscription and forwarded to the downstream subscriber.
extension CachePublisher {
class CachePublisherSubscription<Downstream: Subscriber>: Subscription, Subscriber where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
var downstream: Downstream
var upstream: Upstream
var upstreamSubscription: Subscription?
var read: Read
var write: Write
init(downstream: Downstream, upstream: Upstream, read: #escaping Read, write: #escaping Write) {
self.downstream = downstream
self.upstream = upstream
self.read = read
self.write = write
upstream.subscribe(self)
}
func request(_ demand: Subscribers.Demand) {
if let cachedValue = read() {
downstream.receive(cachedValue)
} else {
upstreamSubscription?.request(demand)
}
}
// keep a reference to the upstream subscription
func receive(subscription: Subscription) {
self.upstreamSubscription = subscription
}
// pass input downstream
func receive(_ input: Input) -> Subscribers.Demand {
self.write(input)
return downstream.receive(input)
}
// pass completion downstream
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
func cancel() {
//TO-DO: Finish cancellation
}
}
}

Related

Combine - bind a stream into another and handle side effects while doing it

I am trying to learn Combine. I know the terms and the basic concept theoretically. But when trying to work with it, I am lost.
I am trying to do is map an Input stream of events to Output stream of state. Is there a way to bind the result of the map to outputSubject? I am trying to make it work with sink but is there a better way?
Also is there an operator equivalent of RxSwift's withLatestFrom?
import Combine
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
mapEventToState(event, with: outputSubject.value)
}
.handleOutput { state in
handleSideEffects(for: state) // Also, how do I access the event here if I needed?
}
.sink {
outputSubject.send($0)
}
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}
Instead of using sink to assign a value to a CurrentValueSubject, I would use assign.
If you want to do something with the values in the middle of a pipeline you can use the handleEvents operator, though if you look in the documentation you'll see that the operator is listed as a debugging operator because generally your pipeline should not have side effects (building it from pure functions is one of the primary benefits.
Just reading the description of withLatestFrom in the RX documentation, I think the equivalent in combine is combineLatest
Here's your code, put into a Playground, and modified a bit to illustrates the first two points:
import Combine
struct Event {
var placeholder: String
}
enum State {
case initial
}
class LearnCombine {
typealias Input = PassthroughSubject<Event, Never>
typealias Ouput = AnyPublisher<State, Never>
let input: Input
var output: Ouput
private var outputSubject: CurrentValueSubject<State, Never>
private var cancellables = Set<AnyCancellable>()
init() {
self.input = PassthroughSubject()
self.outputSubject = CurrentValueSubject(.initial)
self.output = outputSubject.eraseToAnyPublisher()
transformPipeline()
}
private func transformPipeline() {
input
.map { event in
self.mapEventToState(event, with: self.outputSubject.value)
}
.handleEvents(receiveOutput: { value in
debugPrint("Do something with \(value)")
})
.assign(to: \.outputSubject.value, on: self)
.store(in: &cancellables)
}
func mapEventToState(_ event: Event, with state: State) -> State {
return .initial
// Some code that converts `Event` to `State`
}
}
extension Publisher {
func handleOutput(_ receiveOutput: #escaping ((Self.Output) -> Void)) -> Publishers.HandleEvents<Self> {
handleEvents(receiveOutput: receiveOutput)
}
}

Swift Combine: Waiting until subscribed to generate values

I'm trying to write a custom publisher that generates some values. Something like this:
class MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
private let subject = PassThroughSubject<Int, Never>()
func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
subject.receive(subscriber: subscriber)
startSending()
}
func startSending() {
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .finished)
}
}
I'm trying to figure out how to call startSending() automatically after a subscribing attaches, but I'm not sure if I'm doing it right.
I've just been reading about ConnectablePublisher and was wondering if that might help, but I'm not sure how.
Has anyone tried something like this? How did you do it?
As a continuation of this. One of the changes I've been experimenting with is to locally declared the subject instead of having it as a class variable. I think that solves the issue some raised about multiple subscribers.
public func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
let subject = PassthroughSubject<Int, Error>()
defaultValueSubject.receive(subscriber: subscriber)
startSending(to: subject)
}
func startSending(to subject: PassThroughSubject<Int>) {
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .finished)
}

Why .collect() operator in swift Combine always sends .unlimited demand regardless of the demand of upstream publisher?

I have been playing with Combine to understand how it works in more details and I create a custom Publisher, Subscription and Subscriber.
Here's how it looks..
The emoji beamer publisher along with subscription:
struct EmojiBeamerPublisher: Publisher {
typealias Output = String
typealias Failure = Error
private let emojis: [String] = ["👍","❤️","✅","🥰","😍","🚀","😅","🍑","🍞","🎅","❄️","🐻","👀","👄","🦷","✍️","🙏","👨‍💻","🐝","🐛","🦉","🦀","🐍","🐞","🧸"]
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = EmojiBeamerSubscription(output: emojis, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
extension EmojiBeamerPublisher {
private final class EmojiBeamerSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == Failure {
var subscriber: S?
let output: [String]
init(output: [String], subscriber: S) {
self.subscriber = subscriber
self.output = output
}
func request(_ demand: Subscribers.Demand) {
Swift.print("Demand: \(demand)") // Here I receive Unlimited demand
var demand = demand
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] timer in
guard let self = self else { return }
guard demand > 0, let subscriber = self.subscriber else {
timer.invalidate()
self.subscriber?.receive(completion: .finished)
self.cancel()
return
}
demand -= 1
demand += subscriber.receive(self.output.randomElement()! + " \(Date())")
}
}
func cancel() {
subscriber = nil
}
}
}
Here is my Custom subscriber:
final class EmojiBeamerSubscriber<Input, Failure: Error>: Subscriber, Cancellable {
var subscription: Subscription?
let receiveValue: (Input) -> Void
init(receiveValue: #escaping (Input) -> Void) {
self.receiveValue = receiveValue
}
func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(3)) // Here I send only 3 as max demand
}
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
print("Will handle later:", completion)
}
func cancel() {
self.subscription?.cancel()
self.subscription = nil
}
}
extension Publisher {
func myCustomSink(receiveValueHandler: #escaping (Self.Output) -> Void) -> AnyCancellable {
let myCustomSubscriber = EmojiBeamerSubscriber<Self.Output, Self.Failure>(receiveValue: receiveValueHandler)
subscribe(myCustomSubscriber)
return AnyCancellable(myCustomSubscriber)
}
}
As you can see on my custom subscription I request with demand .max(3) if I don't use collect everything works fine, I get an emoji beamed every 5 second after 3 I got a .finish completion.
Works fine (and sends .max(3) demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
However if I simply add .collect() to catch all 3 results at once in an array it just requests with .unlimited demand on my subscription, resulting in a never ending subscription because my demand will never reach zero.
Never complete (and sends unlimited demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.collect()
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
Is there something wrong with my implementation? Or Did I misunderstood the purpose of .collect() operator?
Thank you in advance :)
From the documentation:
This publisher requests an unlimited number of elements from the upstream publisher and uses an unbounded amount of memory to store the received values. The publisher may exert memory pressure on the system for very large sets of elements.
So the behaviour you noticed is the correct one, as collect() sends an unlimited demand upstream.
The unlimited demand causes the demand -= 1 instruction to do nothing, so the demand > 0 check will always pass, resulting into an infinite loop that never sends the completion. You will need an extra condition to make the "collected" stream a finite one.
For infinite streams, the collect(_:) overload (the one that allows to pass a number of items to collect) performs better in regards to the demand, but still requests from upstream more elements than one might expect:
When this publisher receives a request for .max(n) elements, it requests .max(count * n) from the upstream publisher.

Writing a retryIf operator with Swift's Combine framework

I'm getting to know Swift + Swift's Combine framework and wanted to check that my attempt at implementing a retryIf(retries:, shouldRetry:) operator makes sense. In particular, I'm curious if all the .eraseToAnyPublishers are expected/idiomatic.
extension Publisher {
func retryIf(retries: Int, shouldRetry: #escaping (Self.Failure) -> Bool) -> AnyPublisher<Self.Output, Self.Failure> {
self.catch { error -> AnyPublisher<Self.Output, Self.Failure> in
guard shouldRetry(error) && retries > 0 else {
return Fail(error: error).eraseToAnyPublisher()
}
return self.retryIf(retries: retries - 1, shouldRetry: shouldRetry).eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}
Assuming that all the AnyPublishers are ok, when do you want to make your own Publisher struct? For example, the regular Combine operator retry returns a Retry<Upstream> struct rather than an AnyPublisher, but I imagine you could implement it along the same lines as the code above, something like:
extension Publisher {
func doOver(tries: Int) -> AnyPublisher<Self.Output, Self.Failure> {
self.catch { error -> AnyPublisher<Self.Output, Self.Failure> in
guard tries > 0 else { return Fail(error: error).eraseToAnyPublisher() }
return self.doOver(tries: tries - 1).eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
}
You can eliminate the final eraseToAnyPublisher, and thus the heap allocation it requires, by defining your own Publisher. For example:
extension Publisher {
func retry(_ retries: Int, if shouldRetry: #escaping (Failure) -> Bool) -> MyPublishers.RetryIf<Self> {
return .init(upstream: self, triesLeft: retries, shouldRetry: shouldRetry)
}
}
enum MyPublishers { }
extension MyPublishers {
struct RetryIf<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
init(upstream: Upstream, triesLeft: Int, shouldRetry: #escaping (Failure) -> Bool) {
self.upstream = upstream
self.triesLeft = triesLeft
self.shouldRetry = shouldRetry
}
var upstream: Upstream
var triesLeft: Int
var shouldRetry: (Failure) -> Bool
func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
upstream
.catch {
triesLeft > 0 && shouldRetry($0)
? Self(upstream: upstream, triesLeft: triesLeft - 1, shouldRetry: shouldRetry).eraseToAnyPublisher()
: Fail(error: $0).eraseToAnyPublisher()
}
.receive(subscriber: subscriber)
}
}
}
If you want to eliminate the two eraseToAnyPublisher calls inside the catch body, you will have to give up using catch. Instead you will have to implement your own Subscription. Implementing Subscription is much more complicated, because it has to be thread-safe. However, those calls inside the catch body can only happen in the case of an upstream failure, and only one of the calls happens per failure. So if upstream failures are rare, it's probably not worth the effort.

Swift Combine alternative to Rx Observable.create

I have some code that is built using RxSwift, and I'm playing around with converting it to use Apple's Combine framework.
One pattern which is very common is the use of Observable.create for one-shot observables (usually network requests). Something like this:
func loadWidgets() -> Observable<[Widget]> {
return Observable.create { observer in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
observer.onNext(widgets)
observer.onComplete()
}, error: { error in
// publish error on failure
observer.onError()
})
// allow cancellation
return Disposable {
loadTask.cancel()
}
}
}
I'm trying to map that across to Combine and I haven't been able to quite figure it out. The closest I've been able to get is using Future for something like this:
func loadWidgets() -> AnyPublisher<[Widget], Error> {
return Future<[Widget], Error> { resolve in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
resolve(.success(widgets))
}, error: { error in
// publish error on failure
resolve(.failure(error))
})
// allow cancellation ???
}
}
As you can see, it does most of it, but there's no ability to cancel.
Secondarily, future doesn't allow multiple results.
Is there any way to do something like the Rx Observable.create pattern which allows cancellation and optionally multiple results?
I think I found a way to mimic Observable.create using a PassthroughSubject in Combine. Here is the helper I made:
struct AnyObserver<Output, Failure: Error> {
let onNext: ((Output) -> Void)
let onError: ((Failure) -> Void)
let onComplete: (() -> Void)
}
struct Disposable {
let dispose: () -> Void
}
extension AnyPublisher {
static func create(subscribe: #escaping (AnyObserver<Output, Failure>) -> Disposable) -> Self {
let subject = PassthroughSubject<Output, Failure>()
var disposable: Disposable?
return subject
.handleEvents(receiveSubscription: { subscription in
disposable = subscribe(AnyObserver(
onNext: { output in subject.send(output) },
onError: { failure in subject.send(completion: .failure(failure)) },
onComplete: { subject.send(completion: .finished) }
))
}, receiveCancel: { disposable?.dispose() })
.eraseToAnyPublisher()
}
}
And here is how it looks in usage:
func loadWidgets() -> AnyPublisher<[Widget], Error> {
AnyPublisher.create { observer in
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
observer.onNext(widgets)
observer.onComplete()
}, error: { error in
observer.onError(error)
})
return Disposable {
loadTask.cancel()
}
}
}
From what I've learned, the support for initializing an AnyPublisher with a closure has been dropped in Xcode 11 beta 3. This would be a corresponding solution for Rx's Observable.create in this case, but for now I believe that the Future is a goto solution if you only need to propagate single value. In other cases I would go for returning a PassthroughSubject and propagating multiple values this way, but it will not allow you to start a task when the observation starts and I believe it's far from ideal compared to Observable.create.
In terms of cancellation, it does not have an isDisposed property similar to a Disposable, so it's not possible to directly check the state of it and stop your own tasks from executing. The only way that I can think of right now would be to observe for a cancel event, but it's surely not as comfortable as a Disposable.
Also, I'd assume that cancel might in fact stop tasks like network requests from URLSession based on the docs here: https://developer.apple.com/documentation/combine/cancellable
Add an isCancelled operation outside the closure and check it in the future's closure. isCancelled can be toggled with the handleEvent() operator.
var isCancelled = false
func loadWidgets() -> AnyPublisher<[Widget], Error> {
return HandleEvents<Future<Any, Error>> { resolve in
// start the request when someone subscribes
let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
// publish result on success
resolve(.success(widgets))
}, error: { error in
// publish error on failure
resolve(.failure(error))
}
if isCancelled {
loadTask.cancel()
}
).handleEvents(receiveCancel: {
isCancelled = true
})
}
}
and somewhere in the app you do this to cancel the event
loadWidgets().cancel()
Also check this article
Thanks to ccwasden for the inspiration. This replicates Observable.create semantics with a pure Combine implementation without any superfluous entities.
AnyPublisher.create() Swift 5.6 Extension
public extension AnyPublisher {
static func create<Output, Failure>(_ subscribe: #escaping (AnySubscriber<Output, Failure>) -> AnyCancellable) -> AnyPublisher<Output, Failure> {
let passthroughSubject = PassthroughSubject<Output, Failure>()
var cancellable: AnyCancellable?
return passthroughSubject
.handleEvents(receiveSubscription: { subscription in
let subscriber = AnySubscriber<Output, Failure> { subscription in
} receiveValue: { input in
passthroughSubject.send(input)
return .unlimited
} receiveCompletion: { completion in
passthroughSubject.send(completion: completion)
}
cancellable = subscribe(subscriber)
}, receiveCompletion: { completion in
}, receiveCancel: {
cancellable?.cancel()
})
.eraseToAnyPublisher()
}
}
Usage
func doSomething() -> AnyPublisher<Int, Error> {
return AnyPublisher<Int, Error>.create { subscriber in
// Imperative implementation of doing something can call subscriber as follows
_ = subscriber.receive(1)
subscriber.receive(completion: .finished)
// subscriber.receive(completion: .failure(myError))
return AnyCancellable {
// Imperative cancellation implementation
}
}
}