Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combine's Future never completes when flatMapped

Tags:

ios

combine

I have the following simple Future:

class ViewModel {
    var cancellables = Set<AnyCancellable>()
    func test() {
        let trigger = PassthroughSubject<Void, Error>()

       let future =  Future<String, Error> { promise in
                       promise(.success("Future Succeded"))
                   }

        trigger
        .flatMap { future }
        .sink(receiveCompletion: { completion in
            print("completion received \(completion)")
        }, receiveValue: { val in
            print("value received \(val)")
        })
        .store(in: &cancellables)

        trigger.send(())
    }
}

I don't know why it never completes when flat mapped with another publisher (in this case a PassthroughSubject), it only produces the value.

when it's not flat mapped it produces the value and completes normally.

like image 992
JAHelia Avatar asked May 01 '26 18:05

JAHelia


2 Answers

This behavior might look weird but it makes a lot of sense. Completing the Future does not complete the PassthroughSubject. So you can continue sending values through the PassthroughSubject which would result in new Future instances being created and firing. As a rule, a Publisher can only complete or error once. So if completing the Future would trigger the sink completion closure, that means the PassthroughSubject can produce no new values anymore which is not desirable because a PassthroughSubject usually never completes (unless you tell it to directly).

Similar to your example, this code also only triggers the completion once:

var cancellables = Set<AnyCancellable>()

(0..<2).publisher
  .flatMap { _ in return (0..<5).publisher }
  .sink(receiveCompletion: { completion in
    print("completion received \(completion)")
  }, receiveValue: { val in
    print("value received \(val)")
  })
  .store(in: &cancellables)

The reason is that the created publisher is one that will publish two values, and then it completes. If the flatMap publisher would cause the sink completion to be called, that would mean that the (0..<2) publisher completes, except it still has values to send so it's not completed.

So long story short, the beginning publisher decides when the stream completes; not the flatmapped publisher.

like image 145
donnywals Avatar answered May 04 '26 07:05

donnywals


I thought I had this problem but in turns out it was only because I wasn't holding onto the return value from the publisher. As stated in the docs for sink()

The return value should be held, otherwise the stream will be canceled.

My playground code:

var t:Timer?
print(Date())
let _ = Just(33)
    .flatMap { (i) -> Future<Int, Never> in
        return Future<Int, Never> { promise in
            t = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { (t) in
                print("Timer!")
                promise(.success(i * i))
            }
        }
    }
    .sink { (i) in
        print("ok")
        print("\(Date()): \(i) received")
    }

The timer fires but the sink isn't called. Changing the assignment to ...

let pub = Just(33)

...solves it.

like image 39
Hari Honor Avatar answered May 04 '26 08:05

Hari Honor



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!