Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Subscription to promise

In my Angular 7 application I have next function:

  getUserData(uid) {
    return this.fireStore.collection('users').doc(uid).valueChanges().subscribe(data => {
      this.writeCookie(data)
      this.currentUser = data;
    })
  }

And I want to use this function inside another method:

   someMethod() {
      ...
      new Promise(this.getUserData(uid))
         .then(() => {...})
      ...
   }

But I can't do this, because TypeScript throw an error:

Argument of type 'Subscription' is not assignable to parameter of type '(resolve: (value?: {} | PromiseLike<{}>) => void, reject: (reason?: any) => void) => void'. Type 'Subscription' provides no match for the signature '(resolve: (value?: {} | PromiseLike<{}>) => void, reject: (reason?: any) => void): void'.ts(2345)

How can I transform getUserData() method to a promise, or use forJoin instead?

Thanks in advance.

like image 985
Vladimir Humeniuk Avatar asked Apr 09 '19 18:04

Vladimir Humeniuk


3 Answers

subscribe changes the type from Observable to Subscription, thus causing the type error.

What you probably want is to convert your Observable to a Promise, while preserving the function call. You can do this, by piping the Observable through tap and then converting the result with toPromise. Like this:

getUserData(uid) {
  return this.fireStore.collection('users').doc(uid).valueChanges().pipe(
    tap(data => {
      this.writeCookie(data)
      this.currentUser = data;
    }),
    first()
  ).toPromise()
}

Make sure to create a completing pipe, like you can do with the first operator, otherwise the Promise will never resolve.

You can leave out new Promise(...) in your consumer.

like image 95
ggradnig Avatar answered Oct 12 '22 12:10

ggradnig


If you must have .subscription within the getUserData method then this another way.

getUserData(uid): Promise<any> {
    return new Promise((resolve, reject) => {
        this.fireStore.collection('users').doc(uid).valueChanges().subscribe({
            next: data => {
                this.writeCookie(data)
                this.currentUser = data;
                resolve();
            },
            error: err => {
                reject(err);
            }
        });
    });
}

then you can use is like this

someMethod() {
    this.getUserData(uid)
        .then(() => {...
        })
        .catch(e =>{

        });
}
like image 39
CodeNepal Avatar answered Oct 12 '22 12:10

CodeNepal


ggradnig's implementation is the correct solution, however I'd like to go over a more in-depth analysis on WHY it works so there's no confusion if anyone runs into this problem in the future.

When you subscribe to an observable, most of the time you're only passing in one callback function which describes how you want to deal with data from the stream when you receive it. In reality though there are 3 different callbacks that can be included in the observer for different types of events. They are:

  1. next - Called when data is received from the stream. So if you’re making a request to get some pokemon stats, it’s going to call the “next” callback function and pass that data in as the input. Most of the time this is the only data you care about and the creators of rxjs knew this so if you only include 1 callback function into a subscription, the subscription will default to passing in “next” data into this callback.

  2. error - Pretty self explanatory. If an error is thrown in your observable and not caught, it will call this callback.

  3. complete - Called when the observable completes.

If you wanted to deal with all the different types of data emitted from an observable, you could write an observer in your subscription that looks something like this:

this.http.get(“https://pokemon.com/stats/bulbasaur”).subscribe({
    next: () => { /* deal with pokemon data here */},
    error: () => {/* called when there are errors */},
    complete: () => {/* called when observable is done */}
})

Again this is unnecessary most of the time but it’s essential to understand these types of events when we call the “.toPromise()” method on an Observable. When we convert an Observable to a Promise, what’s happening is the Promise is going to resolve with the last “next” data emitted from the Observable as soon as “Complete” method on the Observable is called. This means if the “Complete” callback isn’t called, the Promise will hang indefinitely.

Yeah, I know what you’re thinking: I convert my http requests from Observables to Promises all the time and I never run into a situation where my Promise hangs indefinitely. That’s because the angular http library calls the “Complete” callback on the Observable as soon as all the data is received from the http call. This makes sense because once you receive all the data from the request, you’re donezo. You’re not expecting any more data in the future.

This is different from this situation described in the question where you’re making a call to firestore which I know from experience uses sockets to transmit information, not http requests. This means that through the connection you might receive an initial set of data… and then more data… and then more data. It’s essentially a stream that doesn’t have a definitive end, so it never has a reason to call the “Complete” callback. Same thing will happen with Behavior and Replay subjects.

To circumvent this problem you need to force the Observable to call the “Complete” callback by either piping in “first()” or “take(1)” which will do the same thing, call the “next” callback function with the initial set of data as the input and then call the “Complete” callback.

Hope this is useful to somebody out there cus this problem confused the hell out of me for the longest time.

Also this video is a great reference if you’re still confused: https://www.youtube.com/watch?v=Tux1nhBPl_w

like image 27
Alex Willenbrink Avatar answered Oct 12 '22 10:10

Alex Willenbrink