3

I have a service which loads some data in it's constructor with an Observable. Then at some later time the data can be retrieved with a getter. It should return the data right away if it's present. Or wait for loading to finish if that's still in progress. I came up with following example (code is in Typescript):

class MyService {
    private loadedEvent = new Subject<any>();
    private loaded = false;
    private data: any;

    constructor(
        private dataService: DataService
    ) {
        this.dataService.loadData().subscribe(data => {
            this.data = data;
            this.loaded = true;
            this.loadedEvent.next(null);
        });
    }

    public getDataOrWait(): Observable<any> {
        if (this.loaded) { // A
            return of(this.data);
        }
        return new Observable((observer: Observer<any>) => {
            const subscription = this.loadedEvent.subscribe(() => { // B
                observer.next(this.data);
                observer.complete();
                subscription.unsubscribe();
            });
        });
    }
}

Is there a simpler way to do this? This must be a common pattern.

Also, I think there is a race condition if loading finishes when execution is somewhere between the lines marked A and B (I am not sure if threads are involved here - the data is loaded async however).

nharrer
  • 618
  • 7
  • 21

3 Answers3

4

All you have to do is use a shareReplay() operator:

class MyService {
    public data$: Observable<any>;
    public loaded$: Observable<boolean>;

    constructor(private dataService: DataService) {
        this.data$ = this.dataService.loadData().pipe(
            shareReplay(1);
        );
        this.loaded$ = this.data$.pipe(
           mapTo(true),
           startWith(false)
        );
    }
}

The shareReplay operator is a multi-casting operator that will emit the same previous value to all subscribers. Subscribers will wait until the first value is available.

You can then use that data$ to create a loaded$ observable that will emit false until data$ finally emits a value, and then it will always emit true when the values are ready.

Alternatively, you can have data$ emit a null before data is ready followed by the data. There are some logical benefits downstream that allow you to create new observables for when data is ready or not.

        this.data$ = this.dataService.loadData().pipe(
            startWith(null),
            shareReplay(1);
        );

You have to call myService.data$.subscribe() to trigger the first reading of the stream to make the data ready. You can do that in the constructor, but keep in mind that Angular doesn't create a service until it is first used. If you want the data to be eagerly loaded, then use a resolver in a route or inject the service into a NgModule constructor and subscribe there.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • Wonderful answer. Thank you! I tried both, yours and @Myk Willis answer. I had a hard time deciding which to pick for the accepted answer. I like either of the approaches for maybe slightly different requirements. At the end I picked his, only because he was first. – nharrer Jan 05 '20 at 12:22
  • 1
    @nharrer thank you for up voting. I care more about the votes than accepting. As long as I helped someone learn about operators, then I'm happy. – Reactgular Jan 05 '20 at 12:30
3

It seems that you simply want to logically extend the Observable-based interface of your data service to the clients of your MyService class. You could use a new AsyncSubject, which will emit a single value to all subscribers once it has completed.

class MyService {
  private data: any;
  private dataSubject = new AsyncSubject<any>();

  constructor(
    private dataService: DataService
  ) {
    this.dataService.loadData().subscribe(data => {
      this.data = data;
      this.dataSubject.next(data);
      this.dataSubject.complete();
    });
  }

  public getData(): Observable<any> {
    return this.dataSubject.asObservable();
  }
}

The caller of getData would then do something like:

    service.getData().subscribe((data) => {
      console.log(`got data ${data}`);
    });
Myk Willis
  • 12,306
  • 4
  • 45
  • 62
  • Thank you so much. I didn't know about AsyncSubject. That works very good. Only one little problem. What if this.data is changed at some later time (I didn't have that case in my example). Since dataSubject is already completed, the value never changes. – nharrer Jan 04 '20 at 19:34
  • @nharrer If you want the getData() routine to always return the latest data (as opposed to assuming data is only set once), check out BehaviorSubject. – Myk Willis Jan 04 '20 at 23:25
  • Yes that's what I tried. But since BehaviorSubject always has an initial value there is no wait for loading to be done. But that lead me to use ReplaySubject(1) which behaves like AsyncSubject but .next() can be called afterwards. – nharrer Jan 05 '20 at 12:15
  • @nharrer Your comment was awhile ago but, for what its worth, you could use a ReplaySubject with a constructor parameter of 1 (that's the buffer size). This will work like a BehaviorSubject but subscribers won't get a value until the first call to next(). – Gary Jun 02 '22 at 11:33
-2

First of all you should know that observable guarantees any response from the server will be existing for you, that way your design shall consider that, simply handle your logic in subscribe, and move from that subscription. This is all is done in async mode.

But, if you are getting just single result not streaming, and want to wait till the data is loaded from server completely, then you can use Promise with sync await (this is common pattern) to get the result synchronously. By the way your scenario sounds to be Promise behavior.

If you have to use Observables, you can use forkJoin or flatMap to wait till the date is loaded completely.

Check this awesome answer with perfect comparisons

Is it a good practice using Observable with async/await?

Mohamed Sweelam
  • 1,109
  • 8
  • 22