kosich / rxjs-autorun

Re-evaluate an expression whenever Observable in it emits
MIT License
33 stars 2 forks source link

Support for async expressions? #34

Open loreanvictor opened 1 year ago

loreanvictor commented 1 year ago

I think the scope of applicability of this tool would greatly (and modularly) expand with support of async expressions:

Some notes / open questions:

kosich commented 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 🤔

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:

.

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.

loreanvictor commented 1 year ago

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.