I am assuming that there is some clever ordering of reactives evaluation, I just cannot find where it is implemented in Shiny's sources, nor can I find any documentation about it.
For instance, when a reactive A is depended on by a reactive B through multiple paths, e.g.
A -> x -> y -> z -> B
A -> B
B is evaluated last independently of the order in which the reactives are declared.
Is Shiny using some sort of cycle-aware single-source topological sorting?
When A changes, all items hooked into it are invalidated, this include B.
Therafter, B remains uncalculated until all its inputs (A,z) are calculated.
You can set up your experiment and then view shiny in operation via the reactlog library
Thanks for the example! I have actually written such an example to convince myself that this is what is happening. Thanks also for the suggestion of using reactlog.
However, my question is more about how this is implemented in Shiny rather than what the observable behavior of Shiny is.
If you take my example, B depends directly on A and z while z depends indirectly on A, so clearly some non-greedy dependency graph analysis is at work (i.e. Shiny does not recalculate depth-first or breadth-first). Which is why I am inquiring about the how.
do you mean z depends indirectly on A ? if so I agreed, but I dont know about the need for a non greedy dependency graph analysis . z depends directly on y, so it doesnt need to know anything but the state of y
Yep sorry that's what I meant (I edited the post for future reference). What I mean is that B needs to "know" that z is about to be recalculated because A changed. If you do not take that into account, then B could be recalculating first when A changes, and then a second time when z is updated following an update of x and y.
Now that I think a bit more about it, this ordering is probably implicit in the (greedy, this time) invalidation logic. Meaning the first thing that happens is A marking all its descendants as invalid (not just the direct children), then its descendants re-evaluating one after the other, each being able to do so as soon as all their dependencies are valid again.
Cycles can be handled in rounds by maintaining a list of invalidated reactives that is non-empty at the end the previous round in case a re-evaluated reactive has been invalidated again after it was re-evaluated. If an evaluation produces the same value as the previous one, we skip invalidating the dependents. You eventually get an empty list after a finite number of rounds (fixed point), or you cycle forever.