1

I have two services: one is dependent on the other. Service A makes makes an http call to get data. Service B actually uses that data.

Service A:

@Injectable({
  providedIn: 'root'
})
export class ServiceA {
  data: MyData;

  getData(): Observable<MyData> {
    return this.http.get<Mydata>('http://some.url')
      .pipe(
        tap((data: MyData) => {console.log(`got data');})
      )
    );
  };
}

Service B:

@Injectable({
  providedIn: 'root'
})
export class ServiceB {

  obs = Observable<MyData> = new Observable<MyData>();
  processedData: string[];

  constructor(private serviceA: ServiceA) {
    this.obs = this.serviceA.getData();
    this.obs.subscribe( 
      data => {this.processedData = process(data)},
      error => { /*process error*/ },
      function() { /* maybe mark flag here? */}
      );
  }

  process(endpointData) {
     // Do some business logic to endpointData
     this.processedData = endpointData;
  }

  processedData() {
    // The first time this is called, the observable hasn't completed
  }
}

A client of Service B will call processedData(). Just curious how to elegantly wait on the observable within processData(). The non-async side of me would want to check if the finally part of the observable has been called. If so, just use this.processedData. If not... then what? I suppose I could only subscribe the one time, within processedData, and only on the first call. That still seems not so correct. Thoughts?

kiss-o-matic
  • 1,111
  • 16
  • 32
  • 2
    Have you tried using `toPromise()` to convert observables into a single call that you can await? – chrismclarke Aug 22 '19 at 13:53
  • 1
    This might be of some help. https://stackoverflow.com/questions/44593900/rxjs-one-observable-feeding-into-another – Budhead2004 Aug 22 '19 at 14:02
  • @chrismclarke I did think about toPromise() but most places I've read have advised against using that as a blanket response to an issue. – kiss-o-matic Aug 22 '19 at 14:10

1 Answers1

3

The proper way to wait for an Observable is to not wait, but instead listen.

constructor(private readonly serviceA: ServiceA) {
  this.data$ = this.serviceA.getData().pipe(
     map(data => process(data)),
     shareReplay(1)
  );

  // Immediately subscribe to execute the HTTP call
  this.data$.subscribe({
    error: error => { /* Process error */ },
  });
}

...

processedData(): Observable<MyData> {
  // Return the data "holder".
  // The result will already be there, or in the process of being retrieved
  return this.data$;
}

Using the pipable operator shareReplay means the Observable acts as a cache, returning the latest calculated value on every following subscription.

serviceB.processedData().subscribe({
  next: data => ...
})

The data can be immediately available, or it will require some time to be calculated.

LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • Thanks, this looks like what I want. I was about to edit the post and say, "should I be returning an observable to the caller" which looks like a yes, but with a little extra legwork. – kiss-o-matic Aug 22 '19 at 14:17
  • @kiss-o-matic It's nothing. The fact is the origin (the HTTP call) is an asynchronous operation. Once you start with an asynchronous operation it's pretty difficult to escape the chain. I'd advise embracing the asynchronous nature of Angular. – LppEdd Aug 22 '19 at 14:18
  • @kiss-o-matic the extra legwork is something I would not even account, it's really small and it will avoid all the problems of procedural programming. – LppEdd Aug 22 '19 at 14:20
  • That's the idea... still just takes a while to get used to rxjs. I think I know how it should work, but finding the right operator is not always so straight forward. – kiss-o-matic Aug 22 '19 at 14:20
  • 1
    @kiss-o-matic that's fine when you begin. We were all at that point. Just read a lot. – LppEdd Aug 22 '19 at 14:23
  • To process / manipulate data from `serviceA` in `serviceB` you should add a `map` operator where you call `process(..)` to the observable chain in `serviceB`. – frido Aug 22 '19 at 14:24
  • @fridoo yep, right. Didn't see the function call, thanks! Fixed. – LppEdd Aug 22 '19 at 14:25
  • @LppEdd is that in the initial subscribe()? Maybe for completion sake could I ask to get the answer updated? – kiss-o-matic Aug 22 '19 at 14:27
  • @LppEdd nvm -- I see it. Thanks so much! – kiss-o-matic Aug 22 '19 at 14:27
  • 1
    @kiss-o-matic I will move the `map` in the `constructor`. Updated. – LppEdd Aug 22 '19 at 14:28
  • @kiss-o-matic It realy depends on what you want, whether `process` should be called every time you subscribe to `serviceB.processedData(..)` in a component or just once when the app starts. – frido Aug 22 '19 at 14:33
  • @fridoo In this case the yielded result would always be the same, as the HTTP call is executed a single time only. This is valid if `process` is a pure function. – LppEdd Aug 22 '19 at 14:36
  • @LppEdd Yeah, but we don't know if `process` is a pure function, so the OP has to decide what's fitting. – frido Aug 22 '19 at 14:39
  • @fridoo sure, that's right! – LppEdd Aug 22 '19 at 14:40
  • @LppEdd I made a small edit. Should probably just call process() the one time. I've also changed processedData to be of type Array as that's what's ultimately called. So serviceB.processedData() would return Observable>; – kiss-o-matic Aug 22 '19 at 14:45
  • @kiss-o-matic that's ok! As an hint, try using the `string[]` array syntax. `Array` is redundant pretty much always. – LppEdd Aug 22 '19 at 14:47
  • @LppEdd Noted! I believe there's one last thing here. ServiceB.processedData() returns type of Observable. I going to need a second observable of Observable (and perhaps flatMap()) no? – kiss-o-matic Aug 22 '19 at 14:55
  • @kiss-o-matic mmh wait, I'm getting lost. The HTTP call returns `MyData`, and `process` transforms it to `string[]`, or viceversa? You shouldn't need another observable, just a transformation using `map`, if at all. – LppEdd Aug 22 '19 at 15:01
  • @LppEdd You're correct in your assumption. But ServiceB.processData() returns Observable. It needs to return an observable of ServiceB.processedData (which is type string[]). make sense? – kiss-o-matic Aug 22 '19 at 15:05
  • @kiss-o-matic ok. Why don't you just change `ServiceB.processData` return type then? Or am I missing something. If you need to "map" to another type, just add a `map` step. – LppEdd Aug 22 '19 at 15:09
  • @LppEdd Sorry -- I had declared data$ as Observable so it was moaning about the return type in processedData. Better now. Will chew on this - you've been a huge help. – kiss-o-matic Aug 22 '19 at 15:10