0

In the code below I need to wait on some server calls to initialize objects that are cached as properties on the class. In researching this issue I found several questions that ask how to wait for multiple subscriptions. The answer I chose was to use forkJoin as is shown. However I'm only kicking the can down the road. Now I need to wait on forkJoin. How do I do that? The project I'm working on is not async and I can't go and re-write it.

// Question 2

In theory I could make the constructor private and create a static factory method - however I think I am once again kicking the can as I have no mechanism or place to await the async factory method. If I take this approach how do I tell the Angular DI mechanism to a) await the factory method and b) inject the instance (providedIn 'root' only says create a singleton - does not allow me to actually create the instance).

export class PermissionService {

      public initialization : Promise<void>;  

      private _isAdmin : boolean;
      private appPermissionsDict : object;
      private dataPermissionsDict : object;

      baseUrl = environment.baseUrl + 'permission';

      constructor(private http: HttpClient) {
        this.initialization = this.init();
      }


    async init()
    {
      await forkJoin([
        this.getIsAdmin(),
        this.getAppPermissions(),
        this.getDataPermissions()
        ]).subscribe(([isAdmin, ap, dp]) => {
          this._isAdmin = isAdmin;
          this.appPermissionsDict = Object.assign({}, ...ap.map(x => x.id))
          this.dataPermissionsDict = Object.assign({}, ...dp.map(x => x.id))
          });
          
      //---
      // Need to wait here for properties to be set on the class
      //---
    }
}

Using rxjs 6.5.4 / angular 9.1

  • If I understand your intent correctly, you should instead set up a [dependency provider](https://angular.io/guide/dependency-injection-providers) (along with a provider token) which contains the aggregate result of all the server requests. Then stipulate that `PermissionService` depends on the aforementioned provider token (i.e. via the `deps` property). With that in place, you should be able to access the aggregate result from within `PermissionService` via injecting the provider token. – miqh Jul 13 '21 at 05:42

1 Answers1

0

You can set each async call to properties on your service.

// shareReplay(1) will replay the last emission to any late subscribers
readonly isAdmin$ = this.getIsAdmin().pipe(shareReplay(1));
readonly appPermissionsDict$ = this.getAppPermissions().pipe(shareReplay(1); 
readonly dataPermissionsDict$ = this.getDataPermissions().pipe(shareReplay(1);

Then you can access these properties wherever you inject your service. In your Angular templates, you would do something like this:

<ng-container *ngIf="permissionService.isAdmin$ | async">

</ng-container>

This syntax in templates will deal with the async subscriptions and also handle the unsubscriptions on destroy of the component. This way, you also avoid writing subscriptions using .subscribe manually, which in my opinion is much neater.

I'm not sure if you need to use forkJoin in this case. If you do though, and you need to wait until you have values for isAdmin$, appPermissionsDict$ and dataPermissionsDict$, I would do something like this:

overallPermissions$ = forkJoin({
    isAdmin: this.isAdmin(),
    appPermissionsDict: this.getAppPermissions(),
    dataPermissionsDict: this.getDataPermissions(),
})
// returns observable of { isAdmin: boolean, appPermissionsDict: object, dataPermissionsDict: object }

Then you could use this in your templates:

<ng-container *ngIf="permissionService.overallPermissions$ | async; let overallPermissions">
    <ng-container *ngIf="overallPermissions.isAdmin"></ng-container>
</ng-container>

Again, to reiterate - forkJoin will only emit when all the inner observables emit. If there's an error in any of the inner observables and you don't catch it, it will fail silently. Similarly, if one observable takes a long time to complete, you won't be able to access other inner observable values until the long running one has completed. With the limited information I have about your codebase, I wouldn't use a forkJoin here and just have separate subscriptions for each.

I also think there's nothing wrong with using providedIn: "root" in this case. I imagine for a permissions service you would want a single instance.

deaks
  • 305
  • 2
  • 8
  • Thank you for your answer. Honestly I think it is easier to just `await permissionService.initialization` as is shown in the code in the question. I understand what you say about forkJoin however my goal here is to inject a fully constructed object, not to use one property while another is being constructed. In this case I think saves me from having to do three awaits vs one. –  Jul 13 '21 at 12:53
  • You are missing the point of RxJS. You shouldn't really be using `async/await`. And, as stated in my answer, you can still achieve what you want by using `forkJoin` and injecting your service. – deaks Jul 13 '21 at 13:26