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?