1

I'm new to RxJs and need help/understanding for the following.

I have page that displays current covid cases. I have it setup to poll every 60 seconds. What I'm trying to understand is, if I subscribe to this observable via another new component, I have wait until the next iteration of 60 seconds is complete to get the data. My question is, if I want to share, is there any way to force to send the data and restart the timer?

I don't want 2 different 60 second intervals calling the API. I just want one, and the interval to restart if a new subscriber is initialized. Hope that makes sense.

this.covidCases$ = timer(1, 60000).pipe(
      switchMap(() =>
        this.covidService.getCovidCases().pipe(

          map(data => {
            return data.cases;
          }),
        ),
      ),
      retry(),
      share(),
    );
KingKongFrog
  • 13,946
  • 21
  • 75
  • 124

3 Answers3

3

I think this should work:

const newSubscription = new Subject();

const covidCases$ = interval(60 * 1000).pipe(
  takeUntil(newSubscription),
  repeat(),
  switchMap(() =>
    this.covidService.getCovidCases().pipe(
      /* ... */
    ),
  ),
  takeUntil(this.stopPolling),
  shareReplay(1),
  src$ => defer(() => (newSubscription.next(), src$))
);

I replaced timer(1, 60 * 1000) + retry() with interval(60 * 1000).

My reasoning was that in order to restart the timer(the interval()), we must re-subscribe to it. But before re-subscribing, we should first unsubscribed from it.

So this is what these lines do:

interval(60 * 1000).pipe(
  takeUntil(newSubscription),
  repeat(),
  /* ... */
)

We have a timer going on, until newSubscription emits. When that happens, takeUntil will emit a complete notification, then it will unsubscribe from its source(the source produced by interval in this case).

repeat will intercept that complete notification, and will re-subscribe to the source observable(source = interval().pipe(takeUntil())), meaning that the timer will restart.

shareReplay(1) makes sure that a new subscriber will receive the latest emitted value.

Then, placing src$ => defer(() => (newSubscription.next(), src$)) after shareReplay is very important. By using defer(), we are able to determine the moment when a new subscriber arrives.

If you were to put src$ => defer(() => (console.log('sub'), src$)) above shareReplay(1), you should see sub executed logged only once, after the first subscriber is created. By putting it below shareReplay(1), you should see that message logged every time a subscriber is created.

Back to our example, when a new subscriber is registered, newSubscription will emit, meaning that the timer will be restarted, but because we're also using repeat, the complete notification won't be passed along to shareReplay, unless stopPolling emits.

StackBlitz demo.

Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31
0

This code creates an observable onject. I think what you should do is to add a Replaysubject instead of the Observable.

Replaysubjects gives the possibility to emit the same event when a new subscription occurs.

timer(1, 60000).pipe(
  switchMap(() =>
    this.covidService.getCovidCases().pipe(
      tap(result => {
        if (!result.page.totalElements) {
          this.stopPolling.next();
        }
      }),

      map(data => {
        return data.cases;
      }),
      tap(results =>
        results.sort(
          (a, b) =>
            new Date(b.covidDateTime).getTime() -
            new Date(a.covidDateTime).getTime(),
        ),
      ),
    ),
  ),
  retry(),
  share(),
  takeUntil(this.stopPolling),
).subscribe((val)=>{this.covidcases.next(val)});

This modification results in creating the timer once so when you subscribe to the subject it will emit the latest value immediately

Sándor Jankovics
  • 738
  • 1
  • 6
  • 19
0

You can write an operator that pushes the number of newly added subscriber to an given subject:

const { Subject, timer, Observable } = rxjs;
const { takeUntil, repeat, map, share } = rxjs.operators;

// Operator
function subscriberAdded (subscriberAdded$) {
  let subscriberAddedCounter = 0;
  return function (source$) {
    return new Observable(subscriber => {
      source$.subscribe(subscriber)
      subscriberAddedCounter += 1;
      subscriberAdded$.next(subscriberAddedCounter)
    });
  }
}

// Usage
const subscriberAdded$ = new Subject();

const covidCases$ = timer(1, 4000).pipe(
  takeUntil(subscriberAdded$),
  repeat(),
  map(() => 'testValue'),
  share(),
  subscriberAdded(subscriberAdded$)
)

covidCases$.subscribe(v => console.info('subscribe 1: ', v));
setTimeout(() => covidCases$.subscribe(v => console.info('subscribe 2: ', v)), 5000);
subscriberAdded$.subscribe(v => console.warn('subscriber added: ', v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>

Future possibilities:

  • You can update the operator easily to decrease the number in case you want to react on unsubscribers

!important

The takeUnit + repeat has already been postet by @AndreiGătej. I only provided an alternative way for receiving an event when a subscriber is added.

Running stackblitz with typescript

If the subscriberAdded operator needs some adjustements, please let me know and I will update

Jonathan Stellwag
  • 3,843
  • 4
  • 25
  • 50
  • 1
    Could you elaborate on “You do not need to manually add each subscription to a subject”? Which subscription is manually added to which subject? – Andrei Gătej Jun 07 '20 at 14:02
  • @AndreiGătej I'm really sorry. I mixed something and thought you added every subscription via a custom subject. Your solution works absolutely fine and does the same my custom operator does. Just that it's built in. The only maybe advantage (but not wanted here) is that you could easily add logic for unsubscribing. I will let this answer stay for information reasons and update my comment. Sorry again. – Jonathan Stellwag Jun 07 '20 at 17:58
  • 1
    No worries m8! Couldn't the unsubscribing logic be added with `finalize()` as well? You could be notified about subscriber's unsubscription by placing `finalize()` after `shareReplay()`. One 'caveat' is that the `finalize`'s callback will be invoked when the source completes/errors as well. – Andrei Gătej Jun 07 '20 at 18:05
  • It depends on your wanted behavior. I was thinking about adding a decrease to the subscriberAdded operator whenever a subscriber is left. This would allow you to implement a behavior for every loosing subscriber, not only for the completion or 0 subscriber scenario. I think it just depends on your usecase, but finalize seems a very smart solution for 0 subs :) – Jonathan Stellwag Jun 07 '20 at 18:37