00:21 | <nullvoxpopuli> | just because the arcs are more or less shaped into a cone I think it's easier to see that it's a signal |
10:48 | <Henning> | Hello 👋 Just stumbled upon this issue and thought about reaching out. For VS Code, we have an internal implementation of observables. You can find some demo usage (here). However, I also understand the advantages of transparent observables as presented by this proposal and don't necessarily see a need of this proposal to support use-cases such as ours, where we wanted a more explicit API. There are some things though where we also could use some browser support, mostly around inferring the name of an observable from its variable name/field. |
11:27 | <Henning> | Something novel (to my knowledge) of our observable implementation is the concept of delta information, which can be used to inform observers how or why a value changed. We call observables that don't have a state and just delta information a "signal", which, alone, basically just represents an event with event args. However, observables and signals can be updated/triggered in the same transaction and also be handled at once in the same derived/autorun, which enables some new use cases of observables. This is quite useful when it matters how a certain state was reached, and not just what the state is. I think this "signal" part can be used to bring some rxjs ideas to observables. |
13:59 | <shaylew> | ah, interesting! there seems to be a significant group of existing implementations that use "reader capabilities" of some sort. I'd been wondering if we should provide a facility to move between explicit and implicit readers, something like `trackImplicitly: <R>(read: Reader, f: () => R) => R` `trackExplicitly: <R>(f: (read: Reader) => R) => R` with `get` taking an optional Reader. ... though this does have the unfortunate implication that library code would end up encouraged to use the explicit plumbing version universally (`undefined` plumbs through just fine if the caller wants to be implicit). might be too much bifurcation, and we didn't have many examples/participants using it at scale, which is why we left it out of this first iteration. are you using the readers for async tracking/lexical capture of the tracking environment, or just for the sake of explicitness? |
14:11 | <Henning> |
I agree that mixing explicit and implicit implementations might be tricky and potentially confusing.
For now, just for the sake of explicitness, as we didn't need tracking after an await yet. |
14:12 | <shaylew> | Something novel (to my knowledge) of our observable implementation is the concept of delta information, which can be used to inform observers how or why a value changed. We call observables that don't have a state and just delta information a "signal", which, alone, basically just represents an event with event args. However, observables and signals can be updated/triggered in the same transaction and also be handled at once in the same derived/autorun, which enables some new use cases of observables. This is quite useful when it matters how a certain state was reached, and not just what the state is. I think this "signal" part can be used to bring some rxjs ideas to observables. |
14:14 | <Henning> | Here is an example of how emitting and handling deltas works. |
14:19 | <shaylew> | ah, are your deltas and deriveds both eager? signia's thing is making them lazy (so deltas are lossy -- if you don't read them they'll be discarded instead of piling up), so seems pretty different |
14:20 | <Henning> | the deltas are not lazy per se, but of cause they could be backed by an lazy implementation |
14:20 | <Henning> | signias approach is also very interesting! |
14:21 | <Henning> | the deriveds in our implementation are as lazy as possible, but at the moment they cannot produce deltas (so currently only observable values and signals, i.e. atoms, can produce deltas, which can be handled in deriveds or autoruns, but there is no derived implementation that can produce them yet) |
14:22 | <shaylew> | can they consume them? |
14:22 | <Henning> | yes, they can consume them - the derived example would be analog to the autorun one |
14:23 | <Henning> | deltas can be consumed lazily or eagerly, that depends on the implementation (see autorunHandleChanges, the user can either push all the deltas into an array or combine them using a different method). |
14:25 | <shaylew> | ahh, so the consumer sort of "receives them" eagerly but separately from their reactive body rerunning, so they get to choose whether to buffer or process the information online? |
14:26 | <Henning> | exactly. They are also not allowed to call into other code when they receive the deltas, because other observables might not have received the event at this point yet |
14:27 | <shaylew> | "delta stuff" in some sense or another seems necessary to get an efficient way to (eg) "count how many of these input signals are `true`", so I'm definitely interested in figuring out what minimal-but-sufficient support they'd need from the implementation |
14:28 | <shaylew> | (ideally minimal, sufficient, and not too opinionated -- not guaranteed to be a bar that's clearable...) |
14:28 | <Henning> | Here is a production example (and yes, this derived has some "ugly" side effects and is basically an autorun, but that way callers can wait on the fetch promise) |
14:32 | <shaylew> | do you guarantee some particular order in the changes each thing gets to fold over? |
14:33 | <shaylew> | (I'm squinting at this (on a phone) and realizing it could be cousin to Jane Street `incremental`'s `unorderedFold` nodes) |
14:35 | <Henning> | what do you mean with "gets to fold over"? The only guarantee is that an observer receives all deltas from an observable in the same order. However, if an observable has multiple observers, it is not specified which observer gets the change first. But this does not matter, because the observer is not allowed to interact with other observers at this point. |
14:36 | <Henning> | btw, a countTrues(o: IObservable<boolean>[]): IObservable<number> can also be efficiently implemented using an autorun for each item in o . However, transactions cannot see through such side-effect autoruns anymore |
14:39 | <shaylew> | the view from a single observer that observes multiple observables, I meant -- maybe fold isn't the intended way to think about it, I just saw an "initial summary" and a "summarize previous summary + incoming context into new summary" and jumped to the "ah, folding over a stream" framing |
14:40 | <Henning> | btw, a o that has a side effect which increments/decrements some shared count variable. Then you can create one derived that reads all these deriveds and returns the count field - I think this should give you an optimal solution without deltas. |
14:45 | <shaylew> | don't you lose if you ever have to rerun that one derived that reads all the others, or if you ever have to traverse each edge back towards the N deriveds it reads? but yeah that's sort of what you want -- you lose the topology/scheduling constraint if you just use an autorun, and the derived version tries to recover that |
14:52 | <Henning> |
Right, this would require a derived that does not need to resubscribe to all observables when it reruns. |
14:57 | <Henning> | Btw. our implementation has a different problem (which I guess mobx also has): When a source observable is changed, where n deriveds depend on it transitively, then updating that source observable is O(n), even if the change stops after the first derived. |
15:11 | <shaylew> | y eah that's a thing; it seems pretty inescapable in lazy-deriveds systems. even when you rerun the minimal set of deriveds, you sometimes do more traversal/bookkeeping than would be ideal |
15:12 | <shaylew> | "deep graph" "high fanout" "high fan-in" are the stress tests... along with maybe "toggling which nodes are observed" for systems that have that concept |