rxjs-autorun
rxjs-autorun copied to clipboard
Support for async expressions?
I think the scope of applicability of this tool would greatly (and modularly) expand with support of async expressions:
- It removes the need for creating and then handling higher-order observables when single async operations are intended (which to me seems to be a dominating use case of higher-order observables, basically mapping some observable such as user input to a request).
const a = fromUserInput() const fetched = computed(async () => { const res = await fetch(`https://my.api/${$(a)}`) const json = await res.json() return json })
- Combined with a cancelling behaviour, it would also provide means for most common use cases of flow control:
const a = fromUserInput() const fetched = cpmputed(async () => { const q = $(a) await sleep(200) // --> this debounces for 200ms const res = await fetch(`https://my.api/${$(a)}`) const json = await res.json() return json })
Some notes / open questions:
- This wouldn't work with global tracking functions.
- How would subscription management work? Like what if a source is visited during a cancelled run, after being unsubscribed?
Hey, @loreanvictor 👋
First impression: wow, this is an amazing idea! Second impression: hmm, the open questions you shared are important and it might mess up the rest of the lib 🤔
- I want to try to keep the
$
global, as it helps potential modularity. - With
awaits
and the dynamic model of subscription (we subscribe to observables the moment we see them in the function) it will potentially force us to re-run some async stuff, e.g.:
computed(() => {
let one = await fetch(`https://my.api/${$(a)}`) // < `a` is subbed here, if `a` doesn't instantly provide value — we halt the fn
let two = await fetch(`https://my.api/${$(b)}`) // < same with `b`, but when `b` emits — we will have to re-run the fetch above
return one + two;
})
- We can't cancel the awaits mid-flight, effectively making it kinda a
concatMap
(afaiu from looking at it, though it might be more complicated) - And it doesn't let us dynamically subscribe / unsubscribe to event's (see normal-strong-weak subscription behaviour)
Still, I like the idea. Though I'm not sure it works with the current approach of the lib.
One hack I can think of to keep the tracking functions global and have some control of async process is having yields
instead of the awaits
. E.g.:
const a = fromUserInput()
const fetched = computed(function *() {
yield sleep(200) // --> this debounces for 200ms
const res = yield fetch(`https://my.api/${$(a)}`)
const json = yield res.json()
return json
})
Though I have concerns that:
- it will work out at all
- it will be possible to type (haven't worked with typing generators, quick glancing at TS docs — afaiu it can be possible)
- using
yield
is not too hacky for the users, and we're trying to simplify things, not to make it more complex 😅
.
I thiiink, in current implementation above can be expressed as:
let d = delay(200)(a);
let f = computed(() => fetch`//url/${$(d)}`);
let result = computed(() => $($(f).json()))
(haven't tried this, just drafting it from my mind)
Though this surely looses in clarity both to await
approach and to native Rx' switchMap
.
If you want to experiment with the idea — please feel free to throw a draft PR for further discussion and investigation.
Async is hard.
P.S: Sorry for a late reply here! I'm a bit affected by the Russo-Ukrainian aggression, so I don't have enough time for the project atm.
On the global tracking function, I suspect it can work IF tracking is conducted before async operations.
computed(async () => {
const val = $(a)
await asyncOp(val)
// ...
})
That said, using this pattern in the wild more I quite often find myself in need of canceling the run after some async op (for example, due to the source having emitted newer values in the meantime). The work around I've found in quel is to track values after the async op, and having the tracking function return undefined
if the run is to be cancelled and the tracked value is invalid:
computed(async () => {
await asyncOp()
if ($(a)) {
// do the thing if we still have got the latest value
}
})
There are of course cleaner ways of handling this (throwing some exception for cancelling the run upon tracking), but anyways this pattern would require tracking after the async operation, which wouldn't work with the global tracking function.
A possible compromise would be to allow local tracking function as well, though this requires further consideration.
P.S. I should also note that all of this would be MUCH easier and MUCH MUCH more efficient with static code analysis instead of runtime tracking.
P.P.S. No worries, I hope you and your loved ones are as safe and sound during these hard times as it is possible, and hoping this all would end sooner rather than later.