0

i am building an angular architecture somewhat similar to this : https://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/

I have simply a base class for managing state which manages state for a particular module:

import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map, take } from 'rxjs/operators';

export class StateService<T> {
  private state$: BehaviorSubject<T>;
  protected get state(): T {
    return this.state$.value;
  }

  constructor(initialState: T) {
    this.state$ = new BehaviorSubject<T>(initialState);
  }

  protected select<K>(mapFn: (state: T) => K): Observable<K> {
    return this.state$.asObservable().pipe(
      map((state: T) => mapFn(state)),

    );
  }

  protected clearState(){
    this.state$.next(null as any);
  }

  protected setState(newState: Partial<T>) {
    this.state$.next({
      ...this.state,
      ...newState,
    });
  }
}

So in order to manage state you have to just simply extend this class

import { HttpClient, HttpErrorResponse, HttpHeaders } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, ObservableInput, throwError } from "rxjs";
import { catchError, shareReplay, tap } from 'rxjs/operators';
import { StateService } from "src/app/core/services/state-service.service";
import { AuthHttpClient } from "./auth-httpclient.service";
import { UserInfo } from "./state-models/user-info.model"



@Injectable({
  providedIn: 'root'
})
export class AuthService extends StateService<UserInfo>{

    protected _tokenExpirationTimer:any;
    protected tokenExpirationDuration = 3600;
    public userInfoattempt$ : Observable<UserInfo> = this.select(state => state);

    public oauthTriggerState$ : Observable<boolean> = this.select(state => state.oauthTriggerSignup);
    constructor(){
      super(null as any);
    }

    public autoLogout(expirationDuration:number){
      this._tokenExpirationTimer = setTimeout(() => {
        this.logoutUser();
      }, expirationDuration * 1000);
    }

    public logoutUser(){
      this.clearState();
      localStorage.clear();
      if(this._tokenExpirationTimer)
        clearTimeout(this._tokenExpirationTimer);
      this._tokenExpirationTimer = null;
    }

    protected handleError(errorResp:HttpErrorResponse):ObservableInput<any>{
      if(errorResp.error.errors) {
        console.log("Error:",errorResp);
        return throwError(errorResp.error.errors);
      }
      return undefined as unknown as ObservableInput<any>;
    }

  public loginUser(userCredentials:UserCredentials,provider?:string) {
    return this.api.request<UserSignedinData,UserSignedinDataAdd>( provider ? `api/auth/signin?provider=${provider}` : 'api/auth/signin','post',null as any,userCredentials)
    .pipe(
      tap(val => {
        if(val.isSuccessfull){
          var decodedToken : Partial<UserInfo> = jwt_decode(val.data.Token);
          var user = new UserInfo(
            val.data.Token,
            decodedToken.id,
            decodedToken.email,
            decodedToken.exp,
            decodedToken.username,
            decodedToken.phone
            );
          this.setState(user);
          // add to local storage
          localStorage.setItem("user",JSON.stringify(user));
          //session expiry
          this.autoLogout(this.tokenExpirationDuration);
        }
        else{
          throw new Error("Some Error Occured!");
        }
        //temporarily throw error
      }),
      catchError(this.handleError)
    )
  }

}



The method of interest here is the loginUser, as you can see in the tap operator it basically calls the setState method which calls .next() on behviourSubject in the parent and evidently the new value in reflected to all the subscribers to userInfoattempt$ observable, so when i inject this service in a component and subscribe to userInfoattempt$ i get the new values whenever there's the change, this works fine until i make another service say signinService that extends this authService and i put login method there,

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, shareReplay, tap } from 'rxjs/operators';
import { AuthHttpClient } from '../auth-httpclient.service';
import { AuthService } from '../auth.service';
import { UserCredentials } from '../state-models/user-credentials.model';
import { UserInfo } from '../state-models/user-info.model';
import { UserSignedinDataAdd } from '../state-models/user-signedin-data-add.model';
import { UserSignedinData } from '../state-models/user-signedin-data.model';
import jwt_decode from 'jwt-decode';
import { UserSignedupData } from '../state-models/user-signedup-data.model';
import { UserSignupCredentials } from '../state-models/user-signup-credentials.model';
import { UserSingedupDataAdd } from '../state-models/user-singedupdataadd.model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SigninService extends AuthService {
  public userInfo$ : Observable<UserInfo> = this.select(state => state);

  constructor(private api:AuthHttpClient,private http:HttpClient) {
    super();
   }
  public loginUser(userCredentials:UserCredentials,provider?:string) {
    return this.api.request<UserSignedinData,UserSignedinDataAdd>( provider ? `api/auth/signin?provider=${provider}` : 'api/auth/signin','post',null as any,userCredentials)
    .pipe(
      tap(val => {
        if(val.isSuccessfull){
          var decodedToken : Partial<UserInfo> = jwt_decode(val.data.Token);
          var user = new UserInfo(
            val.data.Token,
            decodedToken.id,
            decodedToken.email,
            decodedToken.exp,
            decodedToken.username,
            decodedToken.phone
            );
          this.setState(user);
          // add to local storage
          localStorage.setItem("user",JSON.stringify(user));
          //session expiry
          this.autoLogout(this.tokenExpirationDuration);
        }
        else{
          throw new Error("Some Error Occured!");
        }
        //temporarily throw error
      }),
      catchError(this.handleError)
    )
  }
  public autoLogin(){
    const user = JSON.parse(localStorage.getItem('user')!) as UserInfo;
    if(!user)
      return;
    this.setState(user);
    var remainingTime = this.tokenExpirationDuration;
    this.autoLogout(remainingTime);
  }
}


this is where the issue occurs, the loginUser method here is same but when i "inject authService (not signinService)" to a component and subscribe to userInfoattempt$ the new value is not reflected on the other hand if i inject signinService to a component and subscribe to userInfo$ the new value is reflected, although both of them are pointing to the same BehaviourSubject, am i missing something here?

Abdullah Sohail
  • 355
  • 4
  • 8
  • Not 100% sure if I understand you correctly.. but.. if you use inheritance like this, every class that extends gets its own instance of the StateService.. right..? Why not simply use DI here? That’s what is for.. – MikeOne Jul 31 '21 at 15:58
  • @MikeOne Hmm interesting so you are saying every class would get its own instance of state service how though? the inheritance hierarchy here is, StateService ---> AuthService --> SigninService and yes about the DI i am not sure how would i be able to utilize it in this scenario, i will tell you what i am trying to acheive here, the objective is to have service for managing state at module level (in this case AuthModule) and a service at page level with methods to manipulate the state like siginservice, so we can inject the authmodule based state service and utilize it wherever we want – Abdullah Sohail Jul 31 '21 at 16:13
  • if AuthService extends StateService and SignInService extends AuthService wouldn't the SignInService have same instance of StateService as AuthService? – Abdullah Sohail Jul 31 '21 at 16:16
  • 1
    Right.. so SigninService and AuthService both get their own instance. If your stateService needs to be a singleton, you can provideIn: root and simply DI it in the constructor of both services.. – MikeOne Jul 31 '21 at 16:18

0 Answers0