Closed visj closed 3 years ago
Early on with S, when I was still playing around with the algorithm, I had something like this, that incremented/decremented a dirty counter. A couple things I remember from the time that made it a bit tricky:
S is a bit push/pull in propagating change, in that we push up from the changed signals, but they may result in running a stale computation before we've reached it. That's why when the computation node is read we need to check if it's stale and update it if so. A pending status makes this a bit more complicated: if we get to a pending node, we need to trace back to its sources and update any that are also pending first, before we know whether the current node needs updating or not. Not super hard, but something. Requires adding a ref from Log
to the parent computation to achieve.
Child computations add more complexity, in that they also enter a kind of pending state if their parent is pending: we don't know yet if the parent will be re-run and the child will be disposed. So we need to propagate the pending state into children and also backtrack to the parent if the child is read while still in a pending state.
So pending flows through both axes of the graph, parentage and sources. BUT, not quite in the same way. For instance, as soon as we know one source has updated we need to update the computation UNLESS its parent was also pending and turns out to be updating as well, as that disposes the child, ignoring the fact that a source changed.
So it gets tricky. It was enough that I decided to explore subclocks as a solution instead.
Thanks for your input! I think I'm on the right track then, I recognized the cases of sources, a pending owner (where a node is pending disposal), and applying upstream computations when accessing a pending computation.
I started out adding a ref to the Log class, but I thought it was easier to keep track of slots instead, e.g. each computation node has an owner slot and dependent slots. In other words, it mimics how you implemented sourceslots but instead tracks nodes in the RootClock update queue. I thought it was easier to implement it this way, but the downside is that it is in no way compatible with subclocks.
But in my (possibly biased after building this...) opinion, between these two, I think there are more use cases for S.track than S.subclock. Some examples where I'm using it:
array.find
, which only triggers dependent computations once the found value changes.I think the most important aspect of it is how it allows you to avoid running unnecessary code. There is an overhead using track compared to S()
, but I think that in many cases the functions being tracked are more often the bottle neck than the cost of building the dependency graph. I haven't used subclocks excessively, but I get the feeling they're most often useful for computations setting signals, which in my opinion is more of an edge case than those I described above. Just my two cents.
I'm maintaining this as a separate fork of S, tinkering around with some ideas. Just wanted to let you know and see if you thought it was interesting, it's not too much work moving what I've written to a pull request in that case. Otherwise I'll close this. Cheers!
Hey again Adam. Just wanted to let you know that I moved my work into a new repository that builds upon your work but explores the conditional branching logic as a solution instead. I merged your ideas of S
and S-array
into one repo as well.
Tracking logic is especially powerful for array operations, as many can stop propagation unless changed, such as every
, find
, indexOf
etc.
I've also explored using mutation propagation for array methods, which allows turning many array operations from O(n)
to O(1)
.
Just wanted to let you know as it is based on your great work!
Hey Adam!
I've been using your projects for a while now and found myself in a similar situation as the one @ryansolid described about conditionally rendering HTML elements (https://github.com/adamhaile/S/issues/21).
I just wanted to let you know that I've created a fork of S 1, and when I hit Ryans issue I added a new function to S,
S.track
.What it does is that it adds the same capacity to a ComputationNode to behave like
S.value
, e.g. downstream computations are only reevaluated once the value of their source changes. I still need to add more tests to ensure that it actually works, and it would be great to hear your thoughts about this functionality (I'm still pretty new to reactive programming so if you have some edge cases where you think you can break it that would be really helpful, I'm struggling a bit to write proper specs).Here is a simple example (I've changed some syntax in my own version, but I use yours here to illustrate how it works):
Basically how it works is that it adds another state to ComputationNodes: PENDING, and then for tracking, instead of traversing through Log marking nodes STALE, we traverse nodes incrementing a pending counter, then after evaluating their sourced Stale node, either setting them as NotPending, or flagging them as Stale. This way, changes still propagate in correct order as every Computation is flagged for incoming changes, but not always evaluated.
I guess you could get this functionality wrapping computations in another DataSignal, but then they wouldn't respond until next tick.
I thought maybe this could be useful for Enumerable computations in S-array, as you could wrap computations in S.track, only triggering chained computations once the sequence changes.
1: I started out forking it to add type information for Closure Compiler, then found it so useful I've started extending it.