2

I want to execute a pre-defined promise (promise 2) recursively after another promise (promise 1) resolves to see when my connection to a peripheral device returns false, following which I can do something else. I'd like to be able to do so using observables. Here's the code I've come up with so far and have 2 questions:

1) Is there a better alternative to using concatMap twice in my code in the reprovision() function,

and

2) Is there a better way to implement my code to do what I've described above?

Here's the code which uses observables to execute promise 1 (only once) and promise 2 recursively and then does something with the result:

reprovision(){
    this.bleMessage = this.prepareMessageService.prepareMessage(
      MessageCategory.customAction, CustomActionConstants.reboot, this.customPayload
    );

    from(this.bleService.bleWrite('custom', this.bleMessage)).pipe(
      concatMap(x => interval(1000)),
      concatMap(x => this.bleService.checkBleConnection1(this.deviceId)),
      takeWhile(x => x, true)).subscribe(
        resp => console.log('still connected to peripheral'),
        err =>  {
          console.log('disconnected from peripheral, do something else');
        }
      );
}

Here's the code for promise 2 (this.bleService.checkBleConnection1), which I want executed recursively until the peripheral is disconnected from my mobile device. The idea behind this promise is that it should resolve as long as my mobile device is still connected to a peripheral, and throw an error once that connection fails.

checkBleConnection1(deviceId: string){
  return BleClient.getConnectedDevices([]) //BleClient.getConnectedDevices([]) returns a promise
  .then(resp => {
    if (!resp.some(elem => deviceId === elem.deviceId)){
      throw new Error('Peripheral connection failed');
    }
    return true;
  })
  .catch(error => {
    throw error;
  });
}
coder101
  • 383
  • 4
  • 21
  • 1
    Did run the code because I see an error in the second concatMap, you should return an observable but 'checkBleConnection1()' return a boolean or throws error. – Saber Bjeoui Mar 28 '22 at 11:46
  • 1
    @SaberBjeoui- checkBleConnection1() actually returns a promise or throws an error. From what I understand, concatMap converts a promise to observable if that's what you feed it. No errors in running the code - works as expected. Console prints "still connected to peripheral" until promise throws an error, at which point console prints "disconnected from peripheral, do something else" – coder101 Mar 28 '22 at 13:25
  • You are right that was not clear in the docs though. I thought we must convert the promise into an observable then return it, but it looks like it does that internally. – Saber Bjeoui Mar 28 '22 at 14:13
  • what `this.bleService.bleWrite` is returning after resolving? – Saptarsi Mar 29 '22 at 08:02
  • @Saptarsi void. Type is Promise – coder101 Mar 29 '22 at 12:42
  • Are you using interval to achieve recursion ? – Saptarsi Mar 29 '22 at 12:45
  • @Saptarsi Yes - interval to achieve recursion. – coder101 Mar 29 '22 at 13:01
  • 1
    Both answers below seem to work well. I guess which one you pick is a matter of style, unless somebody well versed in observables can explain the superiority of one solution over the other. @Saptarsi makes a good point about his solution - unlike with exhaustMap or contactMap, you don't have a queue of observable elements waiting to be processed or ignored entirely when using expand. – coder101 Mar 30 '22 at 20:08

2 Answers2

1

From your post what I understood is

  • You want to check recursively with 1sec interval by calling this.bleService.checkBleConnection1(this.deviceId) infinitely if device is found
  • You don't want check more if device is not found

So for recursive call there is an operator in Rxjs called expand link

To achieve 1 sec interval adding delay(1000) while returning for recursive call

SO here is my version using expand in place of concatMap(x => interval(1000))

using expand there is no need of using takeWhile,so am removing it

reprovision(){
  this.bleMessage = this.prepareMessageService.prepareMessage(
    MessageCategory.customAction, CustomActionConstants.reboot, this.customPayload
  );

  from(this.bleService.bleWrite('custom', this.bleMessage))
    .pipe(
      expand((x) => from(this.bleService.checkBleConnection1(this.deviceId)).pipe(delay(1000)))
    )
    .subscribe({
      next: (resp) => console.log('still connected to peripheral'),
      error: (err) =>
        console.error('disconnected from peripheral, do something else'),
      complete: () => console.log('Completed !'),
    });
}
Saptarsi
  • 796
  • 5
  • 13
  • how does using expand stop the observable stream? If I understand this correctly, it could go on into perpetuity, right? That's not the aim here. The idea is to keep polling for a connection, or lack thereof, and do so with a reasonable frequency level. – coder101 Mar 30 '22 at 16:15
  • with your interval also how are you stopping observable ?`expand` will stop as soon as you throw a error or return empty observable – Saptarsi Mar 30 '22 at 16:33
  • And to maintain a fixed delay I kept delay before calling again – Saptarsi Mar 30 '22 at 16:35
  • using `expand` there will be no request queue because after receiving response only we are calling again and with `delay` – Saptarsi Mar 30 '22 at 17:26
  • thinking about this a bit more, I wonder if `concatMap`, which calls `bleService.checkBleConnection`, is even required, considering `expand` does precisely the same thing (ie, call `bleService.checkBleConnection` recursively). Thoughts? – coder101 Apr 16 '22 at 05:01
  • `concatMap` is not required here,why I added no idea :D . Modified my answer – Saptarsi Apr 17 '22 at 14:01
1

I believe the goal here is to check the connection every 1 second after you've completed the first promise (side note: this is called polling, not recursion, it'd be recursive if you called reprovision() from inside reprovision() or something similar, you can poll recursively but you're not here and generally don't want to unless you have to).

You can't really get rid of the second concatMap because you have to switch into it based on the interval, you can separate and clean up the streams a little bit like this:

const pollConnection$ = interval(1000).pipe(
  concatMap(i => this.bleService.checkBleConnection1(this.deviceId)),
  takeWhile(x => x, true)
);

from(this.bleService.bleWrite('custom', this.bleMessage)).pipe(
  concatMap(x => pollConnection$),
).subscribe(
    resp => console.log('still connected to peripheral'),
    err =>  {
      console.log('disconnected from peripheral, do something else');
    }
  );

but you need to be careful with an operator like concatMap as it can build backpressure which is bad for an app. if these requests can take longer than 1 second to respond, then you'll be building a queue of requests which can deteriorate your application performance. safer alternatives are switchMap and exhaustMap depending on desired behavior

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • Presume you're suggesting the use of switchMap or exhaustMap for the second concatMap, right (`concatMap(x => pollConnection$`)? If yes, I understand how using exhaustMap here would make better sense. But I don't get how switchMap would be useful. Wouldn't switchMap basically keep switching to process the next element from interval(1000) if `concatMap (x=> pollConnection$` takes longer to complete before the next interval output is available, thereby running the risk that we never really get to a point where concatMap is able to feed a value to takeWhile? – coder101 Mar 30 '22 at 16:07
  • 1
    yes I am, the first one doesn't really matter which you use as it only emits once. `switchMap` would be useful if the response time varies and you want to cancel the prior request and try again in the event one takes too long to complete. `exhaustMap` is useful if you expect longer responses and just want to keep waiting and ignore new requests while you wait. – bryan60 Mar 30 '22 at 16:11
  • So with switchMap, I should probably increase the interval frequency a bit, right, so that `this.bleService.checkBleConnection1(this.deviceId)` has at least some chance of completing before the next output from interval? It wouldn't make sense otherwise if `this.bleService.checkBleConnection1(this.deviceId)` always takes longer to complete than it takes for interval to produce the next output. Am I understanding that correctly? – coder101 Mar 30 '22 at 16:30
  • yes, if the inner call can't complete within the interval, `switchMap` would never emit. and `concatMap` would build backpressure. so in either case you'd want to extend the interval or use `exhaustMap` – bryan60 Mar 30 '22 at 16:37