Open loreanvictor opened 1 year ago
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 🤔
$
global, as it helps potential modularity.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;
})
concatMap
(afaiu from looking at it, though it might be more complicated)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:
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.
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).
Combined with a cancelling behaviour, it would also provide means for most common use cases of flow control:
Some notes / open questions: