Subscriber receives new values after subscription to Publishers.Delay publisher has been cancelled

Environment:
Xcode 11.4.1
Xcode 11.5 beta 2
iOS 13.4.1
iOS 13.5 (simulator)
macOS 10.15.4
Sample code
You can find example playground on github

Description
Subscriber receives new values after subscription to Publishers.Delay publisher has been cancelled.

  • Create pipeline with Delay publisher.
  • Wait for upstream to produce value.
  • Cancel subscription before subscriber receives delayed value event.

Playground example:

import Cocoa
import Combine
import PlaygroundSupport

let page = PlaygroundPage.current
page.needsIndefiniteExecution = true

let delayedTimer = Timer.publish(every: 1.4, on: .main, in: .default).autoconnect() // or Just(Date())
    .print("timer")
    .delay(for: .seconds(5), scheduler: RunLoop.main)
    .print("delay")
    .eraseToAnyPublisher()

var sinkCancellable: AnyCancellable?
sinkCancellable = delayedTimer
    .sink {
        print("value \($0)")
    }

DispatchQueue.main
    .asyncAfter(deadline: .now() + .seconds(3)) {
        sinkCancellable = nil
        print("---- cancel ----")
    }

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) {
    print("Playground Finished")
    page.finishExecution()
}

Output

timer: receive subscription: (Timer)
delay: receive subscription: (SUBSCRIPTION_TYPE)
delay: request unlimited
timer: request unlimited
timer: receive value: (2020-05-10 09:07:09 +0000)
timer: receive value: (2020-05-10 09:07:11 +0000)
delay: receive cancel
timer: receive cancel
---- cancel ----
delay: receive value: (2020-05-10 09:07:09 +0000)
value 2020-05-10 09:07:09 +0000
delay: receive value: (2020-05-10 09:07:11 +0000)
value 2020-05-10 09:07:11 +0000
Playground Finished

As you can see provided closure is called after cancellable returned by sink is deallocated.
Is this expected behavior?

This means we can't rely on lifecycle of returned cancellable when subscribing to a publisher.
For example, following code will crash if object is deallocated before delayed value is received.

protocol SomeDependency {
    func someMethod() -> AnyPublisher<Any, Never>
}

class SomeObject {
    private var cancellables = Set<AnyCancellable>()

    private let dependency: SomeDependency

    init(dependency: SomeDependency) {
        self.dependency = dependency
    }

    func perform() {
        dependency.someMethod()
            .sink { [unowned self] value in 
                self.doStuff(with: value)
            }
            .store(in: &cancellables)
    }

    private func doStuff(with value: Any) {
        // some logic
    }
}

I believe it just does as (lightly) documented:

A publisher that delays delivery of elements and completion to the downstream receiver.

It does just that: it passes along every event the Timer fires until the sink is cancelled, with a delay. It may not be that useful other than for debugging; if you want a delay-shaped publisher that drops currently-delayed events at the moment it's canceled, you probably need a different publisher.

Terms of Service

Privacy Policy

Cookie Policy