Swift combine repeat if (with delay) custom publisher - swift

I need to implement repeat if with delay custom publisher for Swift Combine. The purpose of it is to repeatedly poll backend endpoint with delay set from previous response. It could be long polling (max 5min. with 3 - 6sec time period). I tried to use recursive approach, but it is not working consistelty. It makes from 20 to 200 repeats randomly, and then there is fired finished on the former/first subsciption and rest of the subscrptions are finished also. Count of repeats probably depends on memory situation etc.. Any coments or hints how to implement described functionality in reactive way are welcome.
Playground: git#github.com:BenBella/repeatif-publisher-playground.git
enum CustomPublishers { }
extension CustomPublishers {
struct RepeatIf<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
init(
upstream: Upstream, shouldRepeat: #escaping (Upstream.Output) -> Bool,
withDelay: #escaping (Upstream.Output) -> Int
) {
self.upstream = upstream
self.shouldRepeat = shouldRepeat
self.withDelay = withDelay
}
var upstream: Upstream
var shouldRepeat: (Upstream.Output) -> Bool
var withDelay: (Upstream.Output) -> Int
func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
upstream
.print("CustomPublishers.RepeatIf(1)>")
.flatMap { output in
Just((output)).setFailureType(to: Downstream.Failure.self)
.delay(for: .seconds(withDelay(output)),
scheduler: DispatchQueue.global())
}
.flatMap { output in
shouldRepeat(output)
? Self(upstream: upstream, shouldRepeat: shouldRepeat, withDelay: self.withDelay)
.eraseToAnyPublisher()
: Just((output)).setFailureType(to: Downstream.Failure.self)
.eraseToAnyPublisher()
}
.catch { (error: Upstream.Failure) -> AnyPublisher<Output, Failure> in
return Fail(error: error).eraseToAnyPublisher()
}
.print("CustomPublishers.RepeatIf(2)>")
.receive(subscriber: subscriber)
}
}
}
I was able to implement desired functionality with different approach, but I am still interested in any comments regarding to why recursive flatMap doesn't work. Thank you
Working solution:
func retryRequestWithDelay(url: URL) -> AnyPublisher<Response>, AppError> {
let pollPublisher = CurrentValueSubject<Int, AppError>(0)
return pollPublisher.compactMap { [weak self] delay in
return self?.networkingService.request(url)
.delay(for: .seconds(delay), scheduler: DispatchQueue.global())
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.handleEvents(receiveOutput: { [weak self] (response: Response) in
guard let self = self else { return }
if response.status == .pending {
self.pollPublisher.send(response.pollPeriod)
} else {
self.pollPublisher.send(completion: .finished)
}
})
.filter { (response: Response) in
guard response.data.status == .pending else { return true }
return false
}
.eraseToAnyPublisher()
}

You can achieve this via two flatMap() calls:
extension CustomPublishers {
struct RepeatIf<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
init(upstream: Upstream,
shouldRepeat: #escaping (Upstream.Output) -> Bool,
withDelay delay: #escaping (Upstream.Output) -> Int) {
self.upstream = upstream
self.shouldRepeat = shouldRepeat
self.delay = delay
}
private var upstream: Upstream
private var shouldRepeat: (Upstream.Output) -> Bool
private var delay: (Upstream.Output) -> Int
func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
upstream
.flatMap { (output: Output) -> AnyPublisher<Output, Failure> in
guard shouldRepeat(output) else {
return Just(output)
.setFailureType(to: Failure.self)
.eraseToAnyPublisher()
}
return Just(())
.delay(for: .seconds(delay(output)), scheduler: DispatchQueue.main)
.flatMap { () -> RepeatIf<Upstream> in
return RepeatIf(upstream: upstream, shouldRepeat: shouldRepeat, withDelay: delay)
}
.eraseToAnyPublisher()
}
.receive(subscriber: subscriber)
}
}
}
extension Publisher {
func repeatIf(shouldRepeat: #escaping (Output) -> Bool,
withDelay delay: #escaping (Output) -> Int) -> CustomPublishers.RepeatIf<Self> {
.init(upstream: self, shouldRepeat: shouldRepeat, withDelay: delay)
}
}
The outer flatMap will either return the value, if no retry should be made, or it flatMap's over a dummy published whose scope is just to provide a starting point for the delay operator.
Example usage:
// this is a simple publisher that provides an incremented value each time
// a subscription occurs
var cnt = 0
let future = Deferred {
Future<Int, Never> { promise in
cnt += 1
promise(.success(cnt))
}
}
// let's test this baby
let subscription = future
.handleEvents(receiveOutput: { print("\(Date()): original value: \($0)") })
.repeatIf(shouldRepeat: { $0 < 5 }, withDelay: { $0 })
.sink(receiveValue: { print("\(Date()): final value: \($0)") })
// making sure we wait for all events to be received
// and makins sure the subscription doesn't deallocate while we wait
withExtendedLifetime(subscription) {
RunLoop.main.run()
}
The above code prints something like this:
2022-08-23 17:10:10 +0000: original value: 1
2022-08-23 17:10:11 +0000: original value: 2
2022-08-23 17:10:13 +0000: original value: 3
2022-08-23 17:10:16 +0000: original value: 4
2022-08-23 17:10:20 +0000: original value: 5
2022-08-23 17:10:20 +0000: final value: 5

Related

Create a publisher that emits a value on completion of another publisher

I have a publisher that never emits items and only completes or fails with an error (AnyPublisher<Never, Error>). I want to transform that publisher into a publisher that emits a value when the first publisher completes (AnyPublisher<Value, Error>), or passes through any error. I want to create that value after completion of the first publisher. I could do something like this, but it seems quite messy:
func demo() -> AnyPublisher<Int, Error> {
// Using Empty just for demo purposes
let firstPublisher = Empty<Never, Error>(completeImmediately: true).eraseToAnyPublisher()
var cancellable: AnyCancellable?
return Future<Int, Error> { promise in
cancellable = firstPublisher
.sink { completion in
switch completion {
case .failure(let error):
promise(.failure(error))
case .finished:
// some operation that generates value
let value:Int = 1
promise(.success(value))
}
} receiveValue: { _ in
}
}
.handleEvents(receiveCancel: {
cancellable?.cancel()
})
.eraseToAnyPublisher()
}
Can this be done a better way? Something like:
extension AnyPublisher {
func completionMap<T, P>(_: (_ completion: Subscribers.Completion<Self.Failure>) -> P) -> P where P: Publisher, T == P.Output, Self.Failure == P.Failure {
/// ???
}
}
func demo() -> AnyPublisher<Int, Error> {
// Using Empty just for demo purposes
let firstPublisher = Empty<Never, Error>(completeImmediately: true).eraseToAnyPublisher()
return firstPublisher
.completionMap { completion -> AnyPublisher<Int, Error> in
switch completion {
case .failure(let error):
return Fail(error: error).eraseToAnyPublisher()
case .finished:
// some operation that generates value
let value:Int = 1
return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
You could use .append (which returns a Publishers.Concatenate) as a way to emit a value after the first publisher completes.
let firstPublisher: AnyPublisher<Never, Error> = ...
let demo = firstPublisher
.map { _ -> Int in }
.append([1])
The above will emit 1 if firstPublisher completes successfully, or would error out.

How to return a failure inside .map function in a Result

I have a method execute that calls an external API with a callback that receives Result<Data?,Error>. How can I map that optional success to an unwrapped result or an Error?
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
handle(mappedResult)
}
}
This code fails with Invalid conversion from throwing function of type '(Optional<Data>) throws -> _' to non-throwing function type '(Data?) -> NewSuccess'
I was able to do this with a simple switch (below), but I was wondering if throwing a failure inside the .map is possible.
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
switch result {
case .failure(let error):
handle(.failure(error))
case .success(let data):
guard let data = data else {
handle(.failure(NSError(domain: "", code: 0, description: "error")))
return
}
handle(.success(data))
}
}
}
You can convert between throws functions and functions that return Result<Success, Error> by using Result(catching:) and .get().
Here's your original map call:
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
Result.map takes a Result and a function that converts (Success) -> NewSuccess, and returns a Result<NewSuccess, Failure>.
Your map takes a Data (Success), and returns Result<Data, Error> (NewSuccess). So the final type, by plugging in NewSuccess is: Result<Result<Data, Error>, Error>. That's more layers than you want. You want to flatten that to just Result<Data, Error>, and that's where flatMap comes in.
Your answer shows that, but you can also pull this out into a more general-purpose tool. It only works when Failure == Error, because throws is untyped, so you can't limit it to some subset of errors. But that's what you're doing anyway. Here's tryMap:
extension Result where Failure == Error {
func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> {
self.flatMap { value in
Result<NewSuccess, Error> { try transform(value) }
}
}
}
With that, you can rewrite this as:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.tryMap {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
})
}
}
That said, I'd probably be tempted to write it this way:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap { maybeData in
maybeData.map(Result.success)
?? .failure(NSError(domain: "", code: 0, description: "error"))
})
}
}
Or if I wanted someone to be able to actually read it later:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap {
switch $0 {
case .some(let data): return .success(data)
case .none: return .failure(NSError(domain: "", code: 0, description: "error"))
}
}
)
}
}
The advantage of this switch over yours is that you don't have to unwrap and rewrap previous failures.
Apparently, this can be done using flatmap. So in my case:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.flatMap { data in
Result<Data, Error> {
guard let data = data else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
}
handle(mappedResult)
}
}
It's a little confusing, but it is working for me.

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.

Publisher emitting progress of operation and final value

Given I have an SDK which provides the functionality below
class SDK {
static func upload(completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("my_value"))
}
}
}
I am able to a create a wrapper around to make its usage more functional
class CombineSDK {
func upload() -> AnyPublisher<String, Error> {
Future { promise in
SDK.upload { result in
switch result {
case .success(let key):
promise(.success(key))
case .failure(let error):
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
}
Now I'm trying to understand how my CombineSDK.upload method should look like if the SDK upload method also provides a progress block like below:
class SDK {
static func upload(progress: #escaping (Double) -> Void, completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress(0.5)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progress(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("s3Key"))
}
}
}
We need an Output type for your publisher that represents either the progress, or the final value. So we should use an enum. Since the Foundation framework already defines a type named Progress, we'll name ours Progressable to avoid a name conflict. We might as well make it generic:
enum Progressable<Value> {
case progress(Double)
case value(Value)
}
Now we need to think about how the publisher should behave. A typical publisher, like URLSession.DataTaskPublisher, doesn't do anything until it gets a subscription, and it starts its work fresh for each subscription. The retry operator only works if the upstream publisher behaves like this.
So our publisher should behave that way, too:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
<#code#>
}
}
}
Creating the publisher (by calling SDK.uploadPublisher()) doesn't start any work. We'll replace <#code#> with code to start the upload:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subject = PassthroughSubject<Output, Failure>()
subject.receive(subscriber: subscriber)
upload(
progress: { subject.send(.progress($0)) },
completion: {
switch $0 {
case .success(let value):
subject.send(.value(value))
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
}
}
)
}
}
}
Note that we call subject.receive(subscriber: subscriber) before we start the upload. This is important! What if upload calls one of its callbacks synchronously, before returning? By passing the subscriber to the subject before calling upload, we ensure that the subscriber has the chance to be notified even if upload calls its callbacks synchronously.
Note: started writing an answer that's has a largely similar intent to #robmayoff's answer, but using Deferred, so posting here for completeness.
Swift Combine only works with values and errors - there's no separate type for progress. But you can model the progress as part of the output, either as a tuple, as was suggested in another answer, or as a custom enum with both progress and result as cases, which would be my preferred approach.
class CombineSDK {
enum UploadProgress<T> {
case progress(Double)
case result(T)
}
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return subject.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
EDIT
Based on #robmayoff's comment, the above solution doesn't handle synchronous case where subject.send is called before subject is returned.
The solution is largely the same, but it does introduce a small complication of having to capture these values, just in case. This can be done with Record, which will provide a temporary sink to subject
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
var recording = Record<UploadProgress<String>, Error>.Recording()
subject.sink(
receiveCompletion: { recording.receive(completion: $0) },
receiveValue: { recording.receive($0) })
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return Record(recording: recording).append(subject).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
Here is possible approach
extension CombineSDK {
func upload() -> AnyPublisher<(Double, String?), Error> {
let publisher = PassthroughSubject<(Double, String?), Error>()
SDK.upload(progress: { value in
publisher.send((value, nil))
}, completion: { result in
switch result {
case .success(let key):
publisher.send((1.0, key))
publisher.send(completion: .finished)
case .failure(let error):
publisher.send(completion: .failure(error))
}
})
return publisher.eraseToAnyPublisher()
}
}

Swift Combine Chunk Operator

I'm trying to create chunks of a stream in Apple's Combine framework.
What I'm going for is something like this:
Stream a:
--1-2-3-----4-5--->
Stream b:
--------0-------0->
a.chunk(whenOutputFrom: b)
-------[1, 2, 3]---[4, 5]-->
Can this be implemented in Combine?
What you are looking for is the buffer operator in the ReactiveX world.
There is no built in buffer operator (in the ReactiveX sense) in Combine. The built-in buffer is seems to be more like a bufferCount in ReactiveX.
I found this answer by Daniel T, which recreates the buffer operator in RxSwift, and also this cheatsheet, which tells you how to port RxSwift to Combine.
However, the answer by Daniel T uses Observable.create, which isn't available in Combine. I looked a bit deeper, and found this other answer, that recreates Observable.create in Combine.
Combining the three things I've found (no pun intended), this is what I came up with:
// -------------------------------------------------
// from https://stackoverflow.com/a/61035663/5133585
struct AnyObserver<Output, Failure: Error> {
let onNext: ((Output) -> Void)
let onError: ((Failure) -> Void)
let onCompleted: (() -> 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)) },
onCompleted: { subject.send(completion: .finished) }
))
}, receiveCancel: { disposable?.dispose() })
.eraseToAnyPublisher()
}
}
// -------------------------------------------------
// -------------------------------------------------
// adapted from https://stackoverflow.com/a/43413167/5133585
extension Publisher {
/// collects elements from the source sequence until the boundary sequence fires. Then it emits the elements as an array and begins collecting again.
func buffer<T: Publisher, U>(_ boundary: T) -> AnyPublisher<[Output], Failure> where T.Output == U {
return AnyPublisher.create { observer in
var buffer: [Output] = []
let lock = NSRecursiveLock()
let boundaryDisposable = boundary.sink(receiveCompletion: {
_ in
}, receiveValue: {_ in
lock.lock(); defer { lock.unlock() }
observer.onNext(buffer)
buffer = []
})
let disposable = self.sink(receiveCompletion: { (event) in
lock.lock(); defer { lock.unlock() }
switch event {
case .finished:
observer.onNext(buffer)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
buffer = []
}
}) { (element) in
lock.lock(); defer { lock.unlock() }
buffer.append(element)
}
return Disposable {
disposable.cancel()
boundaryDisposable.cancel()
}
}
}
}
// -------------------------------------------------
I think you would be interested in Combine collect() method.
there is variation it it as well such as by time, count or both.
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
where we pass context -> for example global queue
time to wait for it such as 1s in above example
and the count of 10 elements.
Use case would look something like this:
let bufferSubject = PassthroughSubject<Int, Never>()
let cancelBag = Set<AnyCancellable>()
let subscriber = bufferSubject.eraseToAnyPublisher()
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
.sink { value in
print("🚀🚀 value: \(value)")
}
.store(in: &cancelBag)
be sure to test it :)
bufferSubject.send(1)
bufferSubject.send(2)
bufferSubject.send(3)
...
DispatchQueue.asyncAfter(...) {
bufferSubject.send(4)
bufferSubject.send(5)
bufferSubject.send(6)
}