[Fixed] Angular: have side effects and return Observable from a method

Issue

I have this function:

  /**
   * Attempt login with provided credentials
   * @returns {Observable<number>} the status code of HTTP response
   */
  login(username: string, password: string): Observable<number> {
    let body = new URLSearchParams();
    body.append('grant_type', 'password');
    body.append('username', username);
    body.append('password', password);

    return this.http.post(Constants.LOGIN_URL, body, null)
      .do(res => {
        if (res.status == 200) {
          this.authTokenService.setAuthToken(res.json().auth_token);
        }
      })
      .map(res => res.status)
      .catch(err => { return Observable.throw(err.status) });
  }

This function attempts to perform login and returns Observable<number> that the caller can use in order to be notified of the HTTP code of the response.

This seems to work, but I have one problem: if the caller just calls the function without subscribing to the returned Observable, do function is not called and the received auth token is not being saved.

Documentation of do states:

Note: this is different to a subscribe on the Observable. If the
Observable returned by do is not subscribed, the side effects
specified by the Observer will never happen. do therefore simply spies
on existing execution, it does not trigger an execution to happen like
subscribe does.

What I’d like to achieve, is to have this side effect regardless of whether the caller of login() subscribed or not. What should I do?

Solution

As explained by @Maximus, by design, cold Observable (like http call) will not emit any data before you subscribe to them. So your .do() callback will never get called.

Hot Observables on the other hand will emit data without caring if there is a subscriber or not.

You can convert your cold Observable to a ConnectableObservable using the publish() operator. That Observable will start emitting when its connect() method get called.

login(username: string, password: string): Observable < number > {
  let body = new URLSearchParams();
  body.append('grant_type', 'password');
  body.append('username', username);
  body.append('password', password);
  let request = this.http.post(Constants.LOGIN_URL, body, null)
    .do(res => {
      if (res.status == 200) {
        this.authTokenService.setAuthToken(res.json().auth_token);
      }
    })
    .map(res => res.status)
    .catch(err => {
      return Observable.throw(err.status)
    }).publish();
  request.connect();
  // type assertion because nobody needs to know it is a ConnectableObservable
  return request as Observable < number > ; 
}

As @Maximus stated. If the subscription happen after the ajax call as completed, you won’t get notified of the result. To handle this case you can use publishReplay(1) instead of simple publish(). PublishReplay(n) will repeat the last n-th elements emitted by the source Observable to new subscribers.

Leave a Reply

(*) Required, Your email will not be published