alia
alia

Reputation: 459

Unable to share http request between two subscribers

I have an application that gets random jokes every 10 seconds. I'm using interval operator from rxjs for countdown and after 10 seconds i make a http request to get a random joke. The issue is i'm displaying jokes twice in my template using async pipe. I know that doing that will create two new subscriptions and make http request twice. To handle that i tried using shareReplay or publish + refCount , so that only one http request is made. But it calls getRandomJoke service function twice. How to fix this issue.

Code and Demo - https://stackblitz.com/edit/angular-ivy-xvtsrw

random-joke.component.html

<div class="joke-title">{{joke | async}}</div>
<div class="joke-title">{{joke | async}}</div>
<div class="footer">Next Joke in : {{countdown | async}}</div>

random-joke.component.ts

export class RandomJokesComponent implements OnInit {
  joke: Observable<any>;
  restartTimer = new Subject();
  restartInterval = new Subject();
  intervalForJokes: Observable<any>;
  countdown: any;
  countDownTill = 10;
  constructor(private fetchService: FetchUtilService) {}

  ngOnInit() {
    this.startTimer();
    this.getJokesInInterval();
  }
  getJokesInInterval() {
    this.restartInterval.next();
    let intervalForJokes = interval(10000);
    this.joke = intervalForJokes.pipe(
      tap(()=> console.log('getting interval')),
      takeUntil(this.restartInterval),
      publish(),
      refCount(),
      switchMap(() =>
        this.fetchService.getRandomJoke().pipe(
          tap(() => this.startTimer()))
      )
    );
  }
  startTimer() {
    this.restartTimer.next();
    this.countdown = timer(0, 1000).pipe(
      takeUntil(this.restartTimer),
      map(i => this.countDownTill - i)
    );
  }
}

fetch-service

getRandomJoke(): Observable<any> {
    console.log('getting');
    return this.http.get(this.apiUrl).pipe(
      tap(result => console.log(result)),
      // shareReplay(),
      publish(),
      refCount(),
      map((result: any) => result && result.value.joke)
    );
  }

Upvotes: 0

Views: 236

Answers (2)

John Malkoln
John Malkoln

Reputation: 461

Your implementation is overcomplicated. Use shareReplay and start joke fetching immediately by using startWith(0). FetchUtilService.getRandomJoke can be simplified just to http request logic + error handling.

ngOnInit() {
    this.startTimer();

    this.joke = interval(10000).pipe(
      startWith(0),
      switchMap(() =>
        this.fetchService.getRandomJoke().pipe(tap(() => this.startTimer()))
      ),
      shareReplay(),
    );
  }
@Injectable()
export class FetchUtilService {
  private apiUrl = 'https://api.icndb.com/jokes/random';

  constructor(private http: HttpClient) {}

  getRandomJoke(): Observable<any> {
    console.log('getRandomJoke invocation');
    return this.http
      .get(this.apiUrl)
      .pipe(map((result: any) => result && result.value.joke));
  }
}

Demo: https://stackblitz.com/edit/angular-ivy-qpugke

Bonus: restart timer on mouse click

ngOnInit(): void {
    const click$ = fromEvent(document, 'click').pipe(
        debounceTime(400),
        startWith(null), // init timer
    );

    this.timer$ = click$.pipe(
        switchMap(() => interval(1000)), // kills interval on click and starts new one
    );

    const readyToFetch$ = this.timer$.pipe(
        map((seconds) => seconds > 0 && seconds % 10 === 0), // fetch every 10 seconds, skip 0 to avoid joke's fetching on timer restart
    );

    this.joke$ = readyToFetch$.pipe(
        startWith(true), // initial fetch
        filter((readyToFetch) => readyToFetch),
        switchMap(() => this.fetchService.getRandomJoke()),
        shareReplay(),
    );
}

Demo: https://stackblitz.com/edit/angular-ivy-uak1sx

Upvotes: 3

Picci
Picci

Reputation: 17762

If you want to use the same response of an http call, you better think to use Subjects.

The mechanism can be the following.

First you create a service which exposes a method, for intance callRemoteService, to fire the http call and an Observable, for instance httpResp, that will emit the response. httpResp$ is obtained tranforming a private Subject, for instance _resp$ into an Observable using the asObservable() method of Subject.

Then, when the Obvervable linked to the http call emits, you make the internal Subject _resp$ emit as well.

More than one client can subscribe to the public httpResp$ Observable and will receive the same notification when the http call returns.

The code could look like this

private _resp$ = new Subject<any>();
public httpResp$ = this._resp$.asObservable();

public callRemoteService() {
  return this.http.get(this.apiUrl).pipe(
     tap({
        next: (data) => this._resp$.next(data),
        error: (err) => this._resp$.error(err)
     })
  ).subscribe()
}

You may find more inspiration in this article. It talks about React, but the mechanism illustrated there can be applied to Angular too.

Upvotes: 0

Related Questions