Empty() publisher does not send completion

Viewed 534

In this code I am expecting the Empty() publisher to send completion to the .sink subscriber, but no completion is sent.

func testEmpty () {
    let x = XCTestExpectation()

    let subject = PassthroughSubject<Int, Never>()

    emptyOrSubjectPublisher(subject).sink(receiveCompletion: { completion in
        dump(completion)
    }, receiveValue: { value in
        dump(value)
    }).store(in: &cancellables)

    subject.send(0)

    wait(for: [x], timeout: 10.0)
}

func emptyOrSubjectPublisher (_ subject: PassthroughSubject<Int, Never>) -> AnyPublisher<Int, Never> {
    subject
        .flatMap { (i: Int) -> AnyPublisher<Int, Never> in
            if i == 1 {
                return subject.eraseToAnyPublisher()
            } else {
                return Empty().eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}

Why does the emptyOrSubjectPublisher not receive the completion?

1 Answers

The Empty completes, but the overall pipeline does not, because the initial Subject has not completed. The inner pipeline in which the Empty is produced (the flatMap) has "swallowed" the completion. This is the expected behavior.

You can see this more easily by simply producing a Just in the flatMap, e.g. Just(100):

    subject
    .flatMap {_ in Just(100) }
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { value in
        print(value)
    }).store(in: &cancellables)
    subject.send(1)

You know and I know that a Just emits once and completes. But although the value of the Just arrives down the pipeline, there is no completion.

And you can readily see why it works this way. It would be very wrong if we had a potential sequence of values from our publisher but some intermediate publisher, produced in a flatMap, had the power to complete the whole pipeline and end it prematurely.

(And see my https://www.apeth.com/UnderstandingCombine/operators/operatorsTransformersBlockers/operatorsflatmap.html where I make the same point.)

If the goal is to send a completion down the pipeline, it's the subject that needs to complete. For example, you could say

func emptyOrSubjectPublisher (_ subject: PassthroughSubject<Int, Never>) -> AnyPublisher<Int, Never> {
    subject
        .flatMap { (i: Int) -> AnyPublisher<Int, Never> in
            if i == 1 {
                return subject.eraseToAnyPublisher()
            } else {
                subject.send(completion: .finished) // <--
                return Empty().eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}

[Note, however, that your whole emptyOrSubjectPublisher is peculiar; it is unclear what purpose it is intended to serve. Returning subject when i is 1 is kind of pointless too, because subject has already published the 1 by the time we get here, and isn't going to publish anything more right now. Thus, if you send 1 at the start, you won't receive 1 as a value, because your flatMap has swallowed it and has produced a publisher that isn't going to publish.]

Related