ARTICLE AD BOX
I'm trying to understand Combine's subscription lifecycle and why PassthroughSubject drops values when they are sent synchronously inside handleEvents(receiveSubscription:).
import Combine import XCTest class CombineSubscriptionTest: XCTestCase { var cancellable: AnyCancellable? func testSendingInReceiveSubscription() { let value = 1 let subject = PassthroughSubject<Int, Never>() let handler = subject.handleEvents(receiveSubscription: { _ in // Send value synchronously subject.send(value) subject.send(completion: .finished) }) let expectation = expectation(description: "test") var collectedValues = [Int]() cancellable = handler .sink { completion in expectation.fulfill() } receiveValue: { value in collectedValues.append(value) } wait(for: [expectation], timeout: 1.0) // This assertion FAILS - collectedValues is empty! XCTAssertEqual(collectedValues, [value]) } }Result
The test fails - collectedValues is empty even though I called subject.send(value).
My Understanding
I thought that since everything runs on the same thread (main thread in this test) as related to this answer, the value should be received by the sink subscriber. However, it appears the value is being dropped.
My Theory
When I discovered this the .send was being performed on another queue than the .sink and I was getting sporadic failures of this test. Following this, my theory was that if subscription and emission happen on the same queue (in this case, the main queue), there should be no race condition and the value should be delivered. But this test proves that wrong.
Questions
Why does PassthroughSubject drop values sent synchronously inside handleEvents(receiveSubscription:)?
Is it because demand hasn't been established yet? Is this a timing issue within Combine's subscription phases?Is there a difference between Combine's subscription phases?
Does receiveSubscription fire before the downstream subscriber is fully connected? When exactly is "demand" established relative to receiveSubscription?If I add an async dispatch, it works 100% of the time:
let handler = subject.handleEvents(receiveSubscription: { _ in DispatchQueue.main.async { subject.send(value) // This works! subject.send(completion: .finished) } })Why does the async hop fix this?
Is this behavior documented? Where can I read more about Combine's subscription lifecycle and when it's safe to send values to a subject?
Is my understanding of working with PassthroughSubjects correct at all ? In my real use case I am not sending the value synchronously and I am instead passing a closure to a framework which can call it multiple times which will yield multiple calls to .send. Then upon each call to .send a corresponding result is passed in .sink at the client site. Essentially what I need to do is to convert a function with closure which can be called multiple times to a Publisher. Maybe I am approaching this incorrectly to begin with and I don't have to send on the subject which has received the handleEvents callback ?
Expected Behavior
I expected that sending a value to a PassthroughSubject during its subscription phase would be delivered to the subscriber, especially when everything runs on the same thread.
Thanks
