kosich / rxjs-autorun

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

Alternative APIs #3

Open kosich opened 3 years ago

kosich commented 3 years ago

Here we're exploring API suggestions that might be handy or help with handling README#precautions:

  1. Expression with explicit dependencies by @voliva :
const a = timer(0, 1000);
const b = timer(0, 1000);
const c = computed((a, b) => a + b,  a, untrack(b)))

This will react to every change and is side-effect safe It's basically a combineLatest + withLatestFrom + map — most honest way to react to every Observable emission

  1. Async/await syntax:
const a = timer(0, 1000);
const b = timer(0, 1000);
const c = computed(async () => await $(a) + await $(b))

It might react to every change, and is side-effect safe

  1. Provide initial value via tracker by @loreanvictor

Trackers might with initial value (default value?)

const a = interval(1000);
const b = timer(500);
const c = computed(() => $(a, 42) + _(b, 'hi!'));
// > 42 hi!
// > 0 0
// > 1 0
// > …
  1. String tag tracker by @voliva & @loreanvictor:
const a = timer(0, 1000);
const b = tagger`${ a } 🦔`;
// > 0 🦔
// > 1 🦔
// > ...

Alternatives:

  1. we can have tagger to be equivalent of a tracker with concatenation

  2. string expression might be evaluated

--

Suggested APIs can co-exist with the original one, available via different names.

--

If you have an idea — please, add a comment describing it.

Jopie64 commented 3 years ago

Personally I don't think these are actual pitfalls, as long as it's clear what the behavior is for the user.

I even consider 'late subscription' a pro. Contrary to combineLatest() it won't consume resources when it's value is not yet needed. I was even thinking to propose that it should unsubscribe from a dependent observable when it's value is not needed any longer. I have use cases for that. If I'm on my desktop I'll mention one. :)

Also throwing when it's value is unknown should not be a problem (as long as it is clear) cause those functions should be pure like most operator functions should be.

About solution 2, the async/await syntax: consider what would happen when a or b completes before it emits.

kosich commented 3 years ago

@Jopie64 , I agree with your points. I think these suggested APIs could be provided as standalone functions, e.g.: runWithDependency & runAsyncAwait. So that user could choose what type of synchronisation is needed.

Re:

I was even thinking to propose that it should unsubscribe from a dependent observable when it's value is not needed any longer

Yeah, I thought that might be a good thing to have. Though it also might be unexpected, e.g. $( timer(0, 1000) ) % 2 ? $(a) : $(b) would start subscription on each emission. Not sure of this behavior. Would love to discuss your use-case!

Jopie64 commented 3 years ago

So here's a use-case that advocates for unsubscribing unused observables.

[edited by @kosich]: this discussion was moved to #7 and implemented in #10 and #14 . It relates to subscription strength.

loreanvictor commented 3 years ago

Ok so I created this test case for exploring some of the issues mentioned here. It is a re-implementation of this package but:

It seems to be side-effect safe (if I'm understanding correctly what you mean by side-effect safe), it of course cannot handle non-sync expressions (should this utility ever be able to?), etc. Maybe freely experimenting with it helps with this issue.

kosich commented 3 years ago

Uhh, I like your Subject-less approach! Maybe you're right that we don't need that, will play around with that, thanks! Nice work 👍

Regarding subscription strength: I understand your frustration. But since we have a real-life use-case for this, I want to keep it. API-wise for most of the users it is hidden. To simplify support, maybe later we'll be able to split it from computed via some flexible config, needs investigation (what do you think, @Jopie64 ?). Not closing this question, we might change our approach in the future.

I'm adding the $(stream, initial_value) as a suggestion to API extension 🙌

A very nice implementation, thanks! I like that your solution is only 70 lines long, will give a try to a similar refactoring!

loreanvictor commented 3 years ago

thanks! and on subscription strength: I didn't mean that it should be removed or anything, I meant I didn't include it since I didn't find it relevant to this issue in particular. personally I am not super in favor of it, but tbh I do not think it adds that much complexity and since there is realtime use case, I fully understand keeping it around.

kosich commented 3 years ago

My misunderstanding, sorry! I've refactored out Subject as you suggested, check out this PR #28

kosich commented 3 years ago

BTW, it's relatively easy to try these APIs now. Here's an approximation for tracking string tag: (sorry, couldn't resist to trying)

const n = timer(0, 1000);
tracktag`I see ${n} 🦔`.subscribe(console.log);

// implementation
function tracktag(xs, ...os) {
  const [head, ...rest] = xs;
  if (rest.length == 0) return of(head);
  return computed(
    () => head + rest.reduce((a, c, i) => a + (isObservable(os[i]) ? $(os[i]) : os[i]) + c, '')
  );
}

try it on stackblitz

UPD: stackblitz with HTML strings 😅

cc @voliva @loreanvictor

loreanvictor commented 3 years ago

@kosich that looks pretty neat!