1

I have a question about RxJS. I am creating a web app to manage the members of an association. I want to create a button to "reset" the database of a site. The steps are as follows:

  • send an e-mail to all members to re-register
  • Delete data
  • Refresh page

Here's the code I've made, which works, but I know there are a few things wrong. I'm new to RxJS, so I don't quite understand all the principles...

newYear(){
    this.amicalisteService.getAmicalistesValides("").subscribe({
      // Envoyer un mail à l'ensemble des amicalistes valides
      next: amicalistes => {
        for(const amicaliste of amicalistes) {
          const to = amicaliste.email;
          const cc = null;
          const subject = "Adhère à l'AEIR";
          const body = ""
          this.amicalisteService.sendMail(null, to, cc, subject, body).subscribe({
            next: response => {
              console.log('E-mail envoyé avec succès !', response);
            },
            error: error => {
              console.error('Erreur lors de l\'envoi de l\'e-mail :', error);
            }
          });
        }
      },
      complete: () => {
        // Supprimer amicalistes, photos et cartes amicalistes
        this.amicalisteService.getAmicalistes().subscribe(amicalistes  => {
          for(const amicaliste of amicalistes){
            this.amicalisteService.deleteAmicalisteById(amicaliste.id).subscribe();
          }
        });
        this.imageService.getImages("amicaliste").subscribe(images => {
          for(const image of images){
            this.imageService.deleteImageByName("amicaliste", this.imageService.getImageName(image.toString())).subscribe();
          }
        });
        this.imageService.getImages("pdf").subscribe(pdfs => {
          for(const pdf of pdfs){
            this.imageService.deleteImageByName("pdf", this.imageService.getImageName(pdf.toString())).subscribe();
          }
        })
      }
      //Refresh...
    })
  }

I've heard it's not good practice to use subscribe() inside subscribe(), but I can't figure out how to do it differently. There are several things I'd like to keep in this code, however. In the complete, the 3 subscribe() run in parallel, if I'm not mistaken. I'd like to keep that. Otherwise, I understand that using a switchMap could help me, but I can't seem to implement it. Can anyone give me some advice?

Thanks you very much !

Léandre
  • 45
  • 6

2 Answers2

2

Observables are streams of events.

Remote calls (e.g. calling a server to send mails or calling a db to clean up some data) are implemented as streams of events that notify just one event (i.e. the response of the remote call) and then complete or just `error○s.

With RxJs operators you can combine such streams. For instance you do the following:

  • you start with one stream, Stream_A, that emits event_A and then completes
  • you can have a second stream, Stream_B, that emits event_B and then completes
  • and then you combine Stream_A and Stream_B to create a third stream, Stream_A_B that first triggers the execution of Stream_A and emits event_A and, as soon as event_A has been notified, triggers the execution of Stream_B and emits all the events notified by Stream_B, which in this case is just event_B
  • In order to create this combined stream in RxJs we use the oprator concatMap (note: often people use switchMap to concatenate streams - often the result is the same but the meaning and the potential behaviors are slightly different - with sequences of calls to remote services which have to occur one after the other, concatMap is usually the preferred approach)

Another example of combination of more streams to obtain a new stream is the following:

  • There are 2 streams, Stream_1 Stream_2 and Stream_3. Each of these streams emits one value and then completes.
  • We can combine these 3 streams that waits for all 3 streams to emit and complete and then emits only one value, which is the array of all values emitted by the streams, and then complete.
  • With RxJs such new combined stream is obtained with the function forkJoin

Havin said that, with the hope to cast some clarity on RxJs and Observables, here is what I would do in your case

newYear(){
    // assume getAmicalistesValides returns an Observable that emits the result
    // of a remote call
    this.amicalisteService.getAmicalistesValides("")
    // to combine Observables we need to "pipe" operators, i.e. to execute
    // operators one after the other
    .pipe(
      // first thing to do here seems to send an email for each amicaliste
      // assuming we want to send the emails all in parallel, we can first
      // create one Observable for each mail to be sent and then use forkJoin
      // to execute them all in parallel
      // But all this has to happen after the Observable returned by getAmicalistesValides
      // has emitted its value, hence we use concatMap
      concatMap(amicalistes => {
        for(const amicaliste of amicalistes) {
          const to = amicaliste.email;
          const cc = null;
          const subject = "Adhère à l'AEIR";
          const body = ""
          // here we create the array of Observables
          const sendMailObs = this.amicalisteService.sendMail(null, to, cc, subject, body)
          // each of these Observables can print something or react to errors
          .pipe(tap({
            next: response => {
              console.log('E-mail envoyé avec succès !', response);
            },
            error: error => {
              console.error('Erreur lors de l\'envoi de l\'e-mail :', error);
            }))
          });
          // now we trigger the concurrent execution of all sendMail observables
          return forkJoin(sendMailObs)
      }),
      // after having sent the mails you want to do more stuff: delete data, images
      // and so on - assume each of these operations is an Observable
      // you will have to use concatMap and within it create the new Observables
      // and trigger them in parallel using forkJoin, as above
      concatMap(mailSentResults => {
         const deleteDataObs = ....
         const deleteImagesObs = ...
         ...
         return forkJoin([deleteDataObs, deleteImagesObs, // maybe other Obsevables])
      })
    )
    // up to here you have created a new stream, composing various other streams
    // and now is the time to subscribe to this new stream, which is the only stream 
    // you want to explicitely subscribe
    .subscribe({
      next: res => ... // manage the value notified by upstream
      error: err => ... // manage error
      complete: () => ... // do something when all is completed, if required
    })
  }

I hope I have understood your case and all this makes some sense

Picci
  • 16,775
  • 13
  • 70
  • 113
  • This is really clear, thanks for this explaination ! – Léandre Aug 08 '23 at 17:08
  • As a sidenote, the main difference between `concatMap` and `switchMap` is that `concatMap` is complete and exhaustive, where `switchMap` would "switch" to the latest value/snapshot as soon as possible. – Vivick Aug 08 '23 at 17:53
1

Here I basically turn a lot of your observables into one, relying mostly on flatMap (concatMap when it comes to rxjs) and forkJoin to combine multiple observables into one which is analogous to Promise.all for promises.

There's no "monadic" flat map (that I could find) sadly, so we have to map arrays of values to arrays of observable and forkJoin them.

this.amicalisteService.getAmicalistesValides("").pipe(
  concatMap(amicalistes => { // Observable<Amicaliste[]> -> Observable<void[]>
    return forkJoin( // Observable<void>[] -> Observable<void[]>
      amicalistes.map(amicaliste => { // Amicaliste -> Observable<void>
        const to = amicaliste.email;
          const cc = null;
          const subject = "Adhère à l'AEIR";
          const body = ""
          return this.amicalisteService.sendMail(null, to, cc, subject, body)
            .pipe(tap({
              next: response => {
                console.log('E-mail envoyé avec succès !', response);
              },
              error: error => {
                console.error('Erreur lors de l\'envoi de l\'e-mail :', error);
              }
            }));
      })
    );
  }),

  // Then, when the above is completed, execute the rest
  concatMap(() => forkJoin([
    this.amicalisteService.getAmicalistes()
      .pipe(
        concatMap(amicalistes => {
          return forkJoin(amicalistes.map(amicaliste => this.amicalisteService.deleteAmicalisteById(amicaliste.id)));
        })
      ),
    this.imageService.getImages("amicaliste")
      .pipe(
        concatMap(images => {
          return forkJoin(
            images.map(image => this.imageService.deleteImageByName("amicaliste", this.imageService.getImageName(image.toString())))
          )
        })
      ),
    this.imageService.getImages("pdf")
      .pipe(
        concatMap(pdfs => {
          return forkJoin(
            pdfs.map(pdf => this.imageService.deleteImageByName("pdf", this.imageService.getImageName(pdf.toString())))
          )
        })
      )
  ])), 
);
Vivick
  • 3,434
  • 2
  • 12
  • 25
  • 1
    Maybe the monadic behavior you are looking for is provided by `mergeMap` which, by the way, could be used instead of `forkJoin` to control the number of concurrent “live” streams (i.e. the number of concurrent calls to remote services). – Picci Aug 08 '23 at 18:02
  • Sadly `mergeMap` doesn't really cut it. What I wanted was a `Observable -> (T -> Observable) -> Observable`, but `mergeMap` looks like a `Observable -> (T -> U[]) -> Observable` – Vivick Aug 08 '23 at 19:46