AsyncPublisher causes crash in rather simple situation

I finally managed to reduce the crash I was experiencing down to the following:

let a = ObservableObjectPublisher()
Task {
    try await Task.sleep(nanoseconds: 1_000_000)
    a.send()
    a.send()
}
await AsyncPublisher(a).first(where: { _ in true })

which produces the error message: Received an output without requesting demand:

I suppose this is a bug in Combine, so I'll also file a bug report to Apple, but if anyone can help me understand this better so that I can work around it that would be great, because it's problematic for me that after awaiting the publisher in this way I can no longer publish to it without provoking a crash.

Thanks!

4 Likes

That is definitely a bug; it seems like ObservableObjectPublisher is not properly respecting demand. Do you happen to have a feedback # on it?

I think this is the feedback number? FB9975196

Got it routed to the right spot; it looks like ObservableObjectPublisher is expecting everything subscribed to it to be unlimited, I would suggest instead to use a PassthroughSubject instead if possible - that will respect demand correctly.

1 Like

I've been playing with a sample from a WWDC lecture and they seem to do it like this:

  var objectWillChangeSequence:
    AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>>
  {
    objectWillChange
      .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest)
      .values
  }

2 Likes

Hello, I recently experienced the same crash in my app. @Philippe_Hausler is it going to be fixed anytime soon or is it actually expected behaviour?

That screenshot does not tell me very much, but the assertion that it is hitting means that the publisher is breaking the rules for Combine; basically it means that it got a value without any demand applied to the subscription. If you are seeing this with composition I would suggest highly to file a feedback and include a detailed reproduction of the failure.

@Philippe_Hausler this is a minimum reproducible example.

import UIKit

class ViewController: UIViewController {
    let label = UILabel()

    let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(label)

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        Task {
            for await _ in viewModel.objectWillChange.values {
                label.text = viewModel.someString
            }
        }

        viewModel.getCachedString()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        viewModel.getUpdatedString()
    }
}

@MainActor
class ViewModel: ObservableObject {
    @Published var someString = "someValue"

    func getCachedString() {
        Task {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            someString = "cache"
        }
    }

    func getUpdatedString() {
        Task {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            someString = "server"
        }
    }
}

Just create a new Xcode project with Storyboard and paste this code in the ViewController.swift file. Run the app and it will crash :smiling_face_with_tear:

That is the same as the case given in this thread; the workaround is to add a .buffer in the publisher chain.

2 Likes

I see.
Is this behavior a bug though?

Thank you very much for the help!

The bug isn't in the AsyncPublisher but in ObservableObjectPublisher; it breaks the rules normally enforced by the rest of Combine (particularly it expects the downstream to request .unlimited). So I would highly suggest that in that case where you are connecting to a objectWillChangePublisher to put a buffer in there to ensure the proper demand.

3 Likes

For anyone who will encounter this issue in the future, here is a simple fix

extension ObservableObject {
    typealias BufferedObjectWillChangePublisher = Publishers.Buffer<ObjectWillChangePublisher>

    var bufferedObjectWillChange: BufferedObjectWillChangePublisher {
        objectWillChange
            .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest)
    }

    var asyncObjectWillChange: AsyncPublisher<BufferedObjectWillChangePublisher> {
        bufferedObjectWillChange.values
    }
}

Usage:

for await _ in viewModel.asyncObjectWillChange {
    // update the ViewController
}
3 Likes