1

My angular service holds an object _mapView which is initialized after the whole application with all its components and dependencies has been loaded. However, the user might already press buttons etc. which result in get-calls to this mapView object before the app is fully loaded, because the mapView is loaded asynchronously.

My goal is that if the object hasn't been initialized yet, the programm will resume when the mapView is initialized.

@Injectable()
export class MapService {
   private _mapViewPromiseResolveFx;
   private _mapView: Promise<__esri.MapView> = new Promise(function(res, rej){
       this._mapViewPromiseResolveFx = res;
   }.bind(this)); // save a reference to the resolve function so the promise can be resolved from outside its function scope    

   public resolveMapView(mapView: __esri.MapView) {
      this._mapViewPromiseResolveFx(mapView);
   }

   public getMapView() {
      return this._mapView;
   }
}

Somewhen during initialization of the AppComponent, this variable is initialized:

self.mapService.resolveMapView(new MapView(mapViewProperties));

So, whenever I need the mapView, I do:

this.mapService.getMapView().then(function(mapView) {

This seems to be working, but feels like very "hacky" approach and misuse of promises. What other, better options do I have?

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
user66875
  • 2,772
  • 3
  • 29
  • 55
  • "*the mapView is loaded asynchronously*" - then that very loading operation should go inside your promise constructor. Don't load the mapView "*somewhen during initialization of the AppComponent*" and call `resolveMapView` from outside. – Bergi May 01 '18 at 16:08
  • See also [Promises for promises that are yet to be created without using the deferred \[anti\]pattern](https://stackoverflow.com/q/37426037/1048572) – Bergi May 01 '18 at 16:11

2 Answers2

1

This is what Deferred pattern is for. It was implemented in jQuery and is prone to be antipattern but has its uses.

Basically a deferred can be returned instead of just a promise, this is similar to what MapService currently does, but there will be a single deferred object that is responsible for handling its promise:

class Deferred<T = {}> {
  resolve!: (val: T) => void;
  reject!: (err: any) => void;
  promise = new Promise<T>((resolve, reject) => {
    this.resolve = resolve;
    this.reject = reject;
  });
}

@Injectable()
export class MapService {
   mapView = new Deferred<__esri.MapView>();
}

Since it's Angular question, it should be noticed that RxJS is persistently used in Angular applications. It can be perceived as an alternative to promises that does everything that promises do and more. A direct counterpart to promises and deferreds in RxJS is AsyncSubject. It emits a single value to subscribers when a subject is completed.

So will be:

@Injectable()
export class MapService {
   mapView = new AsyncSubject<__esri.MapView>();
}

It should be resolved with:

mapService.mapView.next(...);
mapService.mapView.complete();

If it's already used as a promise, AsyncSubject can be easily switched to a promise because it's an observable that completes with a single value:

const view = await mapService.mapView.toPromise();

In views, both observables and promises can be handled by async pipe, {{ mapService.mapView | async }}.

This seems to be working, but feels like very "hacky" approach and misuse of promises.

There is a possibility that a promise is misused, and this is turns to be an antipattern, like it often is in case of deferreds.

Angular service may act as MV* model, so MapService shouldn't accept map data from the outside but be responsible for initializing and containing map data. In this case it should instantiate MapView during application initialization and expose a promise of MapView instance.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
0

This is fine and explicitly allowed though in general it's not great to perform I/O implicitly.

There are a few things missing:

  • You want to do this._mapView.catch(() => {}) in order to avoid unhandled rejections from showing up (when you create a promise as a rule of thumb always handle the case it ends up rejecting.
  • It is generally confusing to do I/O in initialisation or the constructor. I'd argue it violates the "principle of least astonishment".

I'd recommend something along the lines of a static init async method that initializes the promise on the first call where consumes have an opportunity to handle errors.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504