26

I am definitely sure I am confused here so please any help is appreciated.

Here is my scenario:

I pull from Firestore a document:

return this.afs.collection("events").doc(eventID).snapshotChanges().pipe(
      map( document => {

      })
    );

All is fine up to here.

But inside the map I need a promise to resolve (or not)

For example:

return this.afs.collection("events").doc(eventID).snapshotChanges().pipe(
      map( document => {
        // This is a promise the below part 
        const data = await EventImporterJSON.getFromJSON(document.payload.data())
        return data
      })
    );

I understand that the await cannot happen there. I am very confused how to solve this, perhaps I have not worked long enough with observables and rxjs.

In the end what I am trying to achieve is:

Get the document. Map and process it but inside the process, I need to handle a promise.

I don't want to return that promise to the caller though.

Does this make sense?

Or have I structured this completely wrong?

Jimmy Kane
  • 16,223
  • 11
  • 86
  • 117

2 Answers2

36

This is a typical use-case for mergeMap or concatMap:

return this.afs.collection("events").doc(eventID).snapshotChanges().pipe(
  mergeMap(document => {
    // This is a promise the below part 
    return EventImporterJSON.getFromJSON(document.payload.data())
  })
);

However, you can also use async - await because operators such as mergeMap handle Observables, Promises, arrays, etc. the same way, so you can just return a Promise in mergeMaps project function it will work fine.

Typically, you don't need to use multiple awaits in a single method because the more "Rx" way of doing things is chaining operators, but if you want, you can because the async method returns a Promise and RxJS will handle it like any other Promise.

const delayedPromise = () => new Promise(resolve => {
  setTimeout(() => resolve(), 1000);
})

of('a').pipe(
  mergeMap(async v => {
    console.log(1);
    await delayedPromise();
    console.log(2);
    await delayedPromise();
    console.log(3);
    await delayedPromise();
    return v;
  })
).subscribe(console.log);
// 1
// 2
// 3
// a


Live demo: https://stackblitz.com/edit/rxjs-3fujcs
Poode
  • 1,692
  • 1
  • 15
  • 20
martin
  • 93,354
  • 25
  • 191
  • 226
  • Thanks, as always Martin you give one of the most comprehensive answers. I ll mark yours as the accepted but any thoughts for Kevins answer? – Jimmy Kane Dec 06 '18 at 11:30
8

Observables can be seen as a layer up to promises, why don't you use your promise this way ? like this :

let getDataFromJson(payloadData){
    return from(EventImporterJSON.getFromJSON(payloadData());
}

return this.afs.collection("events").doc(eventID).snapshotChanges().pipe(
  map(document=>document.payload.data),
  switchMap( payloadData=> getDataFromJson(payloadData)))
.subscribe(result=>{
    //final result
});

1 pipe your first observable with map just to simplify your returner value

2 switchMap to another observable which will be your promise as an Observable ( with the "from" operator);

The map operator is made for improve result in synchronous and "pure" way like return only few properties of an object or filter a data, here you want to chain two async operation so I suggest you to keep it in a rx approach

Kevin ALBRECHT
  • 494
  • 4
  • 13