How to write async operations in providers

I’m confused on the best practice method to write providers for async operations, and I need some help. Besides thinking of them as ways to persist data across pages, I consider them as ‘gateways’ to various external entities, such as firebase or http… therefore, I try to put all code that communicates externally into their own provider (i.e. firebase-auth, firebase-database, firebase-storage, etc)

Unfortunately, I suck at doing this.

My problem always comes up with how to manage and coordinate async operations between the provider and the page.

Here are some examples of some things I’ve played with:

  1. Promise wrapped in a promise method- In the provider I’ve written promises wrapped in promises, where the inside promise is the database/http/etc call, and the outside promise is the function that the calling page will use to coordinate async functions (using .then())

Example code in the provider.

public getPeople() {
        return new Promise((resolve, reject) => {
            this.storage.executeSql("SELECT * FROM people", []).then((data) => 
                { 
                  console.log(data); 
                  resolve(people); 
                }, (error) => { reject(error); }); 
         }); 
    }

You would then kick off synchronous events in the calling page with .then() on the returned promise.


  1. Return a promise method- In the provider, the code is there just to set up the database/http/etc promise, that is just returned to the calling page where it uses .then() to coordinate operations.

Example code in the provider.

public getPeople() {
            return this.storage.executeSql("SELECT * FROM people", []) 
            }

You would then kick off synchronous events in the calling page with .then() on the returned promise.


  1. EventEmitters method - All of the code for performing an async operations is written in the provider. The .then() is in the provider and passes the results back to the page via an event. The subscribe event, then, coordinates actions in the page itself.

Example code in the provider.

public getPeople() {
        this.storage.executeSql("SELECT * FROM people", []).then((data) => 
            { 
              this.events.publish('getPeople', data);        
            } 
        }

You would then kick off synchronous events in the calling page by subscribing to the event emitter.

I like the emitter method… but I rarely, if ever see it used in tutorials or in github. Typically, I see the first method… but it seems odd to write a promise inside of another promise.

What method do you use? Is there better way to do this?

1 is not dry and silly for that matter. U find this a lot on this forum but is not because of best practice (although I have seen some cases doing a new Promise to convert a callback method to Promise)

2 is what I do and recommend

3 is basically transforming your app into a universal service bus where you will have a hard time keeping track of all event labels as well as data types. And in this you are basically converting the promise to an untyped, unmanageable and meaningless observable (not literraly). Quite ugly

3 Likes

I basically agree with @Tommertom if these are the only options we are given, but I would suggest a #4 that he hinted at: actually do convert the promise to an observable. The primary reason for this is that promises can only be one-night stands; observables can be lasting relationships. Consider this idiom:

class Provider {
  private _foo = new BehaviorSubject<Foo>(undefined);
  constructor(private _storage: Storage, private _http: HttpClient) {}
  getFoo(): Observable<Foo> { return this._foo.asObservable(); }
  setFoo(foo: Foo) { this._foo.next(foo); }
  loadFooFromStorage(): void { this._storage.get('foo').then(foo => this.setFoo(foo)) }
  loadFooFromEther(): void { this._http.get().subscribe(foo => this.setFoo(foo)) }
}

class Page {
  foo: Foo;
  constructor(private _foos: FooProvider) {
    this._foos.getFoo().subscribe(foo => this.foo = foo);
  }    
}

Any time the app-wide Foo changes, from any source and for any reason, Page and everybody who is doing something similar automatically gets the new data with zero additional effort required on anybody’s part. Try doing that with promises.

4 Likes

Thanks for your input. I’m definitely trying to wrap my head around using providers instead of coding all interactions in the page itself. Managing the asynchronous aspects seems to be the most difficult part between those worlds.

I’m not sure if this is a naming convention thing or an actual design structure thing, but I would do this differently. I design consumers first and providers second, and consumers (generally pages) don’t care and shouldn’t know where the data is coming from. A consumer should not be required to import firebase-auth, firebase-database, firebase-storage.

Instead, I organize providers based on what they provide, not where it’s being provided from. So that means PeopleProvider, JetpackProvider, ReservationProvider, &c. In addition to making consumers easier to read and write in the first place, this allows seamless swapping (and mocking) data sources without any corresponding modification to consumer code.

3 Likes

I hadn’t thought of the emitters being makeshift observables…You’re right, observables would definitely be preferable.

can you speak a little bit more about that? what design decisions pushed you in that direction?

The reason I had designed my providers with the idea of, “ Gateway to where the data came from” was based on the notion that I could swap out functionality with a different web service if needed. I.e. I could change firebase – database to mysql– Database and only have to re-code one provide… just maintain the methods.

Or… maybe that what you’re saying? That the naming of the provider should be independent of firebase, sql, mongo…As long as it’s providing the same data?

Right, therefore it should not have the “where” in its name. Specifically, having both something named “storage” and something named “database” would make me (when writing a consumer) instantly wonder which one I need to use to get People from. Slicing it that way makes writing providers easy, but consumers hard, whereas slicing by what instead of from where makes writing producers a bit harder, but consumers much easier, and generally there are N consumers in a typical app for each producer, so that’s a net win.

Agree to option 4 as the ideal solution.

This example I like a lot as it shows using the observable pattern for settings of a global style

https://devdactic.com/dynamic-theming-ionic/

Outstanding. Thanks for the explantation!

Two quick things:

  1. Your option (1) has an “official” name: the Explicit Promise Construction Antipattern. As you might guess, it’s considered bad practice. In my own code, the only time I’ve had to use “return new Promise” was when I was promisifying something that was not originally a Promise. ES6-ifying vanilla JS code, turning an old-style callback into a Promise.

  2. I solve the “where” issue by having a provider called “AuthService” or “DatabaseService.” Then I have that service inject an under-the-hood service that DOES have the name of the provider, like “FirebaseManager.” That way, if I ever want to change the backend, I can swap out that under-the-hood provider, and the main app does not change at all.

3 Likes

Aaron, thanks for the reply and the help. The anti-pattern that you referenced is exactly why I posted this question. I stumbled across it a couple of days ago and have been trying to figure out the best way to get around it. #2 seemed like an odd way to do it… and the #4 method of wrapping a promise in an observable seemed similar to the idea of wrapping a promise in a promise.

Anyway, I plan on sticking with #2 and #4 to follow best practices. I’m very disappointed that the eventemitter method was trounced so hard. It seemed the cleanest way to keep all the service code in the provider while still allowing for calling page sequencing. sigh

Anything Ionic’s Events can do can be done better using Observables.

2 Likes

Maybe you already know this, but an Angular EventEmitter is different from an Ionic Event. Using EventEmitters is fine, probably even has less overhead than rxjs Observables in a lot of situations. But for performance reasons, it’s best to avoid globally published Ionic Events.

What you get from an Observable instead of an EventEmitter is a much richer library of operators, so you can do in a couple lines what it might take a paragraph to do with an EventEmitter.

If your output is super simple and you don’t need to manipulate it (like True/False a button was pressed) use an EventEmitter. If your output is sophisticated, or you might need to sort it, etc., use an Observable.

1 Like

lol. Don’t worry rapropos… I trust your opinion. I’m just coping through the ‘depression’ phase of the stages of grief. Fortunately, ‘Acceptance’ is next. :slight_smile:

I did not know this. I appreciate the help and advice.

‘Acceptance’ is nexT

Once through the valley of despair and acceptance you will find yourself in an enlightened stage of bliss, happiness and extreme delight building next level (anti)patterns

:grinning:

But with this loadFooFromStorage() or loadFooFromEther() has to be called first before calling getFoo() to get the latest data right?

“latest data” is sort of subjective. What I’m primarily concerned about in this thread is efficiently ensuring that all parts of an app that want to hear about updates get them in a timely fashion without having to do extra work.

So in that sense, simply being subscribed to the Observable exposed in the provider makes sure you (as a page, for example) always have what the app considers to be the “latest” data.

In a more macro sense, yes, I agree with you that in order to goose the provider into fetching the data from somewhere, something has to happen, but how and where that something gets triggered is totally up to you and there’s not a canonical answer.

It could be explicitly triggered by a user action like a refresh button. It could happen at app startup. It could happen periodically. It could happen each time the app is brought back to the foreground. Any of those might be the best choice for certain situations.

The key in this discussion, though, is that regardless of whatever does trigger fetching of new data from somewhere, everybody who cares about that gets seamlessly and automatically notified.