1

Note: Please scroll down to the "Update" as the issue was boiled down to a @Injectable service being instanciated more than once.

I have a resolver that loads businesses:

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> | Promise<any> | any {
  this._logger.debug('Loading application data.');
  return forkJoin([
    this._userService.getMe(),
    this._businessService.getAll()
  ]).pipe(
    tap(([user, businesses]) => {
      this._appState.user = user;
      this._appState.businesses = businesses;
    }),
    finalize(() => this._logger.debug('Application data loaded.'))
  );
}

There is an ApplicationState that has two BehaviorSubject members _business and _businesses:

constructor() {
  this.businessChange = this._business.asObservable();
  this.businessesChange = this._businesses.asObservable();
}

set business(business: BusinessModel) {
  console.log(business);
  this._business.next(business);
}

get business(): BusinessModel {
  return this._business.getValue();
}

set businesses(value) {
  this._businesses.next(value);
  console.log(this.businesses);
}

get businesses(): Array<BusinessModel> {
  return this._businesses.getValue();
}

As you can see, businesses of said state gets set in the resolver. business however depends on the route and gets set in a (lazy-loaded) module depending on that route:

ngOnInit() {

  this._appState.businessesChange.subscribe(
    (b) => {
      console.log('businessesChange');
      console.log(b);
    }
  );

  this._subscriptions.add(
    this._activatedRoute.params
      .subscribe((params) => {
        this._routeParams = params;
        const businessId: number = parseInt(params['businessId'], 10);
        const businesses = this._appState.businesses;
        console.log(`businessId ${businessId}`);
        console.log('available businesses');
        console.log(businesses);
        this._appState.business = businesses.find(b => b.id === businessId);
      })
  );
}

This is the relevant log output:

2019-08-28T09:11:04.989Z DEBUG [..~df7ced28.js:428] Loading application data.
ngx-logger.js:251 2019-08-28T09:11:04.992Z DEBUG [main.js:398] Fetching /me
application.state.ts:56 Set businesses:
application.state.ts:58 (3) [{…}, {…}, {…}]
ngx-logger.js:251 2019-08-28T09:11:06.781Z DEBUG Application data loaded.
businesses.component.ts:43 businessesChange
businesses.component.ts:44 []
businesses.component.ts:47 businessId 4
businesses.component.ts:48 available businesses
businesses.component.ts:49 []
application.state.ts:46 Set business:
application.state.ts:47 undefined
ngx-logger.js:251 2019-08-28T09:11:08.386Z DEBUG Place is null. No action.

As you can see, businesses gets set and contains three elements. Then we see that Application data loaded. from the resolver. After that, we see the _activatedRoute.params subscription stating that the route reveals a businessId of 4 but then.. this._appState.businesses is all of a sudden an empty list - the initial value of the BehaviorSubject even though we printed that out a few moments earlier.

Since I am logging/debuggin the set methods as well, I can see that this value does not get changed after being set to that list of three elements.

I have no idea what's going on here. What could possibly cause this?


What I already tried:

  • Restart the server
  • Use the debugger (but I printf-debug anyway)

Update

Okay I have boiled the issue down to the ApplicationState being instanciated more than once:

By giving the ApplicationState a simple ID (timestamp):

constructor() {
  this.businessChange = this._business.asObservable();
  this.businessesChange = this._businesses.asObservable();
  this.timestamp = moment().format('MM.SS SSS');
  console.log(`${this.timestamp} ApplicationState created`);
}

and changing the log-output in the set and get methods, I get this:

08.09 095 ApplicationState created
ngx-logger.js:251 2019-08-28T09:51:16.098Z DEBUG Loading application data.
ngx-logger.js:251 2019-08-28T09:51:16.099Z DEBUG [main.js:398] Fetching /me
:4200/#/businesses/4/calendar:1 This site does not have a valid SSL certificate! Without SSL, your site's and visitors' data is vulnerable to theft and tampering. Get a valid SSL certificate before releasing your website to the public.
application.state.ts:60 08.09 095 Set businesses:
application.state.ts:62 (3) [{…}, {…}, {…}]
application.state.ts:38 08.14 145 ApplicationState created
ngx-logger.js:251 2019-08-28T09:51:16.161Z DEBUG Application data loaded.
businesses.component.ts:43 businessesChange
businesses.component.ts:44 []
businesses.component.ts:54 businessId 4
businesses.component.ts:55 available businesses
businesses.component.ts:56 []
application.state.ts:50 08.14 145 Set business:
application.state.ts:51 undefined
ngx-logger.js:251 2019-08-28T09:51:16.178Z DEBUG Place is null. No action.
calendar.component.ts:105 

So there is a 08.09 095 (A) and a 08.14 145 (B) instance of ApplicationState.

A gets the businesses set but further B is used apparently.

I checked, ApplicationState gets imported only once in my ApplicationModule:

providers: [
  // Services
  ApplicationState,
  ApiServices,
  ApplicationResolver,
]

So why is this happening? Shouldn't this be Singleton? Is it because this goes over a lazy-loaded module?

But most importantly: Why did this work for the last few weeks?

I also added providedIn by now:

@Injectable({providedIn: 'root'})
export class ApplicationState {

but it still behaves the same.

Stefan Falk
  • 23,898
  • 50
  • 191
  • 378
  • What if you actually subscribe to the behaviorsubjects when retrieving the value, are the businesses still emtpy? – Jelle Bruisten Aug 28 '19 at 09:33
  • @JelleBruisten Just tried it.. it's still empty O_o – Stefan Falk Aug 28 '19 at 09:37
  • @JelleBruisten I updated the log-output and the code. `businessesChange` is the observable of `_businesses`. – Stefan Falk Aug 28 '19 at 09:39
  • Seems like the behaviorSubject value is not updated, the behaviorsubject should always put out the latest value even on a new subscribtion. Therefore It seems maybe the applicationState object is not the same as the one used before? – Jelle Bruisten Aug 28 '19 at 09:52
  • @JelleBruisten Yes, I just checked this (see my update). Apparently it gets instanciated twice? Should this happen? I thoguth injectables are singleton and get instanciated only once? – Stefan Falk Aug 28 '19 at 09:59
  • .. and ffs I don't know how long I am doing it like this already. This worked for weeks and like an hour ago it broke. I can't recall doing something related to this behavior. I implemented a FormControl and all of a sudden.. boom .. – Stefan Falk Aug 28 '19 at 10:00
  • Indeed it seems like because it's lazyloaded/in other modules https://angular.io/guide/singleton-services#the-forroot-pattern – Jelle Bruisten Aug 28 '19 at 10:06

1 Answers1

1

Okay, the answer why this happened was apparently due to lazy loading. It seems to have something to do how the dependency tree gets build or something like that.

I do not know the details and I would accept any anser which is more detailed about this topic.

Anyway, I thought it should be enough to provide the service in a parent module. Because it makes sence, doesn't it?

Turns out that this really has to be provided in the app.module.ts. I have another module, called application.module.ts (should not be confused) where I provided ApplicationState. application.module.ts is lazy loaded but also has some routes which are again lazy loaded e.g. businesses.module.ts. So my hierarchy looks something like this:

app.module.ts
|_ application.module.ts  // lazy, providers: [ApplicationState]
|  |_ businesses.ts       // lazy, uses ApplicationState too
|_ public-pages.module.ts // not lazy

and what I had to change was to set

providers: [ApplicationState]

in app.module.ts.

This is not ideal imo but since lazy loading works like this I'll have to deal with it.

Apparently this is a known issue (https://github.com/angular/angular/issues/12869) but there is also a possible solution on stackoverflow: https://stackoverflow.com/a/41293784/826983

Stefan Falk
  • 23,898
  • 50
  • 191
  • 378