Home > other >  Rxjs - Chain http calls with Observables
Rxjs - Chain http calls with Observables

Time:12-17

I want to make 3 http calls which returns a boolean value. What I want is to make the next call only if the result of the previos call is true

IE:
call1() -> if false, stop, if true:
call2() -> if false, stop, if true:
call3()

now read results in a way that indicates which call returned false if so.

my first attemp with switchMap:

    this.call1()
      .pipe(
        switchMap((r1) => (r1 ? this.call2() : of(false))),
        switchMap((r2) => (r2 ? this.call3() : of(false)))
      )
      .subscribe((r) => {
        console.log('inside subscribe:', r);
      });

the problem here that now inside the subscribe I get:
'inside subscribe:' false
and I can't know if the false result is from call1,2 or 3.

so my solution is to save the results in external array, like so:

    // seriel execution with switchMap, we can use each result to the next call
    //   save all results
    const results = [false, false, false];
    this.call1()
      .pipe(
        // save stage1 result
        tap(r1 => results[0] = r1),
        // if true, make call2, else return false
        switchMap((r1) => (r1 ? this.call2() : of(false))),
        // save stage2 result
        tap(r2 => results[1] = r2),
        // if true, make call3, else return false
        switchMap((r2) => (r2 ? this.call3() : of(false))),
        // save stage3 result
        tap(r3 => results[2] = r3),
      )
      .subscribe((r) => {
        console.log('inside subscribe:', r); // r is the latest value results[2]
        console.log('all results:', results) // array of all stages [true,false,false]
      });

  }

and it seems to work, but maybe there is another way without external work? only with rxjs operators?

StackBlitz link:
https://stackblitz.com/edit/angular-rxjs-playground-zjnh7u?devtoolsheight=33&file=app/app.component.ts

CodePudding user response:

Maybe something like this can work

this.call1()
  .pipe(
    switchMap((r1) => (r1 ? this.call2() : 
             of(false).pipe(tap(() => console.log('r1 returns false')),
    switchMap((r2) => (r2 ? this.call3() : 
             of(false).pipe(tap(() => console.log('r2 returns false'))),
    tap(respOfR3 => if(!respOfR3) {console.log('r3 returns false')})
  )
  .subscribe((r) => {
    console.log('inside subscribe:', r);
  });

At the same time consider that the sequence of booleans notified by your first Observable implicitly contains the info you are looking for. In fact the position of the first false you encounter reflects the first call that has returned false.

CodePudding user response:

I would attach the source to the outcome - simplest thing is a tuple [number, boolean], where the first element in the tuple/array is the source. A variation of the switchMap solution is:

    this.call1()
      .pipe(
        map((result) => [1, result]),
        switchMap((r1) => (r1[1] ? this.call2().pipe(map((result) => [2, result])) : of(r1))),
        switchMap((r2) => (r2[1] ? this.call3().pipe(map((result) => [3, result])) : of(r2)))
      )
      .subscribe((r) => {
        console.log('inside subscribe:', r);
      });

This has a pattern and is a bit verbose, we can do better:

    function attachSource(source: number, target: Observable<boolean>): Observable<[number, boolean]> {
      return target.pipe(map((result) => [source, result]));
    }

    attachSource(1, this.call1())
      .pipe(
        switchMap((r1) => (r1[1] ? attachSource(2, this.call2()) : of(r1))),
        switchMap((r2) => (r2[1] ? attachSource(3, this.call3()) : of(r2)))
      )
      .subscribe((r) => {
        console.log('inside subscribe (try2):', r);
      });

And if we generalize a little more, given the following:

    function attachSource<T>(source: number, target: () => Observable<T>): Observable<[number, T]> {
      return target().pipe(map((result) => [source, result]));
    }

    function series<T>(failed: (arg: T) => boolean, ...factories: (() => Observable<T>)[]): Observable<[number, T]> {
      return factories.reduce((agr, cur, index) => {
        if (index === 0) {
          return attachSource(index 1, cur);
        } else {
          return agr.pipe(switchMap((prevResult) => failed(prevResult[1]) ? of(prevResult) : attachSource(index 1, cur)));
        }
      }, empty<[number, T]>());
    }

It can be as simple as:

    series((x: boolean) => x, this.call1.bind(this), this.call2.bind(this), this.call3.bind(this))
      .subscribe((r) => {
        console.log('inside subscribe (try3):', r);
      });

CodePudding user response:

Here is a very clean solution using expand operator for recursive calls

const results = [false, false, false];
let count = 0;
this.call().pipe(
    tap(response => {
      results[count] = response
      count  
      }
    ),
    expand((response) => response && count < 3
               ? this.call()
               : EMPTY
    )
).subscribe((r) => {
    console.log('inside subscribe:', r); // r is the latest value results[2]
    console.log('all results:', results) // array of all stages [true,false,false]
});

And you don't really need multiple call function (call1, call2, call3), only one will be enough

call(): Observable<boolean> {
 let bool: boolean = this.randBool();
 return of(bool);
}

private randBool(): boolean {
  return Math.random() < 0.5
}

https://stackblitz.com/edit/angular-rxjs-playground-zvfpay?file=app/app.component.ts

CodePudding user response:

Why wouldn't you just do something like this ?

    public ngOnInit() {
    this.call1().subscribe((r1) => {
      console.log('r1 result : ', r1);
      if (r1) {
        this.call2().subscribe((r2) => {
          console.log('r2 result : ', r2);
          if (r2) {
            this.call3().subscribe((r3) => {
              console.log('r3 result : ', r3);
            });
          }
        });
      }
    });
   }

If you will add up other calls later, it can become heavy though

  • Related