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
Ye, actual radio signals are shaped like a rope with adhd
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).
While our implementation/API is heavily inspired by MobX, it tries to include some of our learnings of using MobX in other projects, mostly by making many things less magic and more debuggable. Since most of VS Code uses plain simple fields and change-events, we wanted to make it very clear what is observable and what not and when a derived/autorun subscribes to something observable. Observable usage is not yet very widespread in the VS Code source and it is mostly driven by me, but it is already used in core components, such as some editor contributions and the diff editor and so far the very explicit API style paid off.

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>

though this does have the unfortunate implication that library code would end up encouraged to use the explicit plumbing version universally

I agree that mixing explicit and implicit implementations might be tricky and potentially confusing.

are you using the readers for async tracking/lexical capture of the tracking environment, or just for the sake of explicitness?

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.
I've been looking into a couple of very different delta-based systems (https://signia.tldraw.dev/, and separately differential dataflow which if you squint is very related) and might have to go chew on yours for a bit to tell where it places
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 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
... however that might be circumvented by having a derived for each item in 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>

don't you lose if you ever have to rerun that one derived that reads all the others

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