Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Observation event passing in reactiveswift

I've read the docs multiple times and need clarification...

Given the snippet below:

let signal: Signal<Value,Error>

//call this observer y
signal.take(first: 1).observeValues{ (value) in
  //intended strong capture on self. this is the only one that retains self so if this observer is triggered and completes, self should dealloc
 self.doSomethingElse(value) //trivial call, no async or thread hopping
}

//call this observer x
signal.take(duringLifetimeOf: self).observeValues{ [unowned self] (value) in //is this safe or better to use weak and guard against it? 
   self.doSomeProcess(value) //trivial call, no async or thread hopping
}

If signal is triggered and notifying its observers for value event:

1) Observer y would be notified before x (assumption because it is first observed so earlier in queue)

2) Since y would complete after processing the value, self should dealloc afterwards

Question:

Which events would x receive (in order):

  • value and completion? Is it guaranteed that self would still be alive while the value event is processed?

  • completion only? I doubt this is the case but kindly refer some doc if so. Because completion events aren't propagated immediately.

Would using different Schedulers for both x and y would make an effect on the outcome?

Lastly, am I introducing race? I doubt this because reactiveSwift does not introduce concurrency unless explicitly stated by the dev.

like image 313
Joshua Francis Roman Avatar asked Nov 07 '22 17:11

Joshua Francis Roman


1 Answers

I put together a little sample console app to test this. As I said in my comment, take(first: 1) delivers the completion event synchronously immediately after passing along the 1 value event, which means ys reference to self will go away before any values are delivered to x. Assuming that's the only strong reference to self, x won't receive any values.

import Foundation
import ReactiveSwift
import ReactiveCocoa

class MyClass {
    init(signal: Signal<String, Never>) {
        //call this observer y
        signal.take(first: 1).observeValues{ (value) in
            //intended strong capture on self. this is the only one that retains self so if this observer is triggered and completes, self should dealloc
            self.doSomethingElse(value) //trivial call, no async or thread hopping
        }

        //call this observer x
        signal.take(duringLifetimeOf: self).observeValues{ [unowned self] (value) in //is this safe or better to use weak and guard against it?
            self.doSomeProcess(value) //trivial call, no async or thread hopping
        }
    }

    func doSomethingElse(_ value: String) {
        print("Something Else: \(value)")
    }

    func doSomeProcess(_ value: String) {
        print("Some Process: \(value)")
    }
}

let (signal, input) = Signal<String, Never>.pipe()
_ = MyClass(signal: signal)

input.send(value: "1")
input.send(value: "2")

Sure enough, doSomeProcess is never called:

Something Else: 1
Program ended with exit code: 0

The key thing to remember about ReactiveSwift, is that everything happens synchronously unless you explicitly specify otherwise with a specific set of operators or with your own code. So the take operator doesn't send along the one value event and then somehow "schedule" the delivery of the completion event for later. Both the value and completion event delivery happens during the upstream signal's delivery of the value event, and the deallocation of the observer and its references happens before signal has finished delivering its first event.

When you say "completion events aren't propagated immediately" I assume you are talking about the portion of the APIContracts file that talks about how failures and interruptions are propagated immediately. This is simply noting that many operators pass these events on immediately even if they are asynchronous or time-shifting operators.

The take operator is not a time-shifting or asynchronous operator. And in this case, the operator is not propagating a completion event from the upstream signal; rather, it is generating the completion event itself, and it is doing so synchronously immediately after it propagates the value event.

Am I introducing a race?

You are correct that ReactiveSwift does not introduce asynchrony or concurrency on its own, so there is no "race" here in the traditional sense. However, I believe the API contract for Signal does not guarantee that events are delivered to observers in the order they started observing. So this code's behavior is technically undefined and could change in future versions of ReactiveSwift.

Would using different Schedulers for both x and y have an effect on the outcome?

Now this actually would introduce a race because take's completion event would be delivered on whatever scheduler you set up for that observer, and that event's delivery would trigger the deinit of self.

like image 70
jjoelson Avatar answered Nov 15 '22 07:11

jjoelson