kosich / rxjs-autorun

Re-evaluate an expression whenever Observable in it emits

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Alternative APIs

kosich opened this issue · comments

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.

commented

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.

@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!

commented

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.

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

  • $, _ are passed as arguments
  • the behavior is mostly like combineLatest + map + startWith + withLatestFrom (I say mostly because it still allows for late subscription, like if there is an observable that is tracked through the second run instead of the first).
  • it does not have the subscription strength feature (but honestly it seems that can be added independently and is not related to this).

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.

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!

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.

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

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

@kosich that looks pretty neat!