Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 cache http request using the power of observables

I've found a number of approaches to cache reactive observables and, more specifically, the results of http requests. However, I am not fully satisfied with the proposed solutions because of the reasons below:

1. This answer https://stackoverflow.com/a/36417240/1063354 uses a private field to store the result of the first request and reuses it in all subsequent calls.

the code:

private data: Data;    
getData() {
    if(this.data) {
        return Observable.of(this.data);
    } else {
        ...
    }
}

The sad thing is that the power of observables is completely ignored - you do all the stuff manually. In fact I wouldn't look for a proper solution if I was satisfied with assigning the result to a local variable/field. Another important thing which I consider a bad practice is that a service should not have a state - i.e. should have no private fields containing data which are changed from call to call. And it's fairly easy to clear the cache - just set this.data to null and the request will be reexecuted.

2. This answer https://stackoverflow.com/a/36413003/1063354 proposes to use ReplaySubject:

    private dataObs$ = new ReplaySubject(1);

    constructor(private http: Http) { }

    getData(forceRefresh?: boolean) {
        // If the Subject was NOT subscribed before OR if forceRefresh is requested 
        if (!this.dataObs$.observers.length || forceRefresh) {
            this.http.get('http://jsonplaceholder.typicode.com/posts/2').subscribe(
                data => this.dataObs$.next(data),
                error => {
                    this.dataObs$.error(error);
                    // Recreate the Observable as after Error we cannot emit data anymore
                    this.dataObs$ = new ReplaySubject(1);
                }
            );
        }

        return this.dataObs$;
    }

Looks pretty awesome (and again - no problem to clear the cache) but I am not able to map the result of this call, i.e.

service.getData().map(data => anotherService.processData(data))

which happens because the underlying observer has not called its complete method. I'm pretty sure that a lot of reactive methods won't work here as well. To actually get the data I have to subscribe to this observable but I don't want to do it: I want to get the cached data for one of my components via a resolver which should return an Observable (or Promise), not a Subscription:

The route

{
    path: 'some-path',
    component: SomeComponent,
    resolve: {
      defaultData: DefaultDataResolver
    }
}

The Resolver

...
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Data> {
    return this.service.getData();
}

The component is never activated because its dependency is never resolved.

3. Here https://stackoverflow.com/a/36296015/1063354 I found a proposal to use publishLast().refCount().

the code:

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}

This satisfies my demands for both caching and resolving BUT I haven't found a clean and neat solution to clear the cache.

Am I missing something? Could anyone think out a better way to cache reactive observables being able to map their results as well as refresh the cached data once it's no longer relevant?

like image 860
Ultimacho Avatar asked Oct 29 '22 09:10

Ultimacho


1 Answers

This simple class caches the result so you can subscribe to .value many times and makes only 1 request. You can also use .reload() to make new request and publish data.

You can use it like:

let res = new RestResource(() => this.http.get('inline.bundleo.js'));

res.status.subscribe((loading)=>{
    console.log('STATUS=',loading);
});

res.value.subscribe((value) => {
  console.log('VALUE=', value);
});

and the source:

export class RestResource {

  static readonly LOADING: string = 'RestResource_Loading';
  static readonly ERROR: string = 'RestResource_Error';
  static readonly IDLE: string = 'RestResource_Idle';

  public value: Observable<any>;
  public status: Observable<string>;
  private loadStatus: Observer<any>;

  private reloader: Observable<any>;
  private reloadTrigger: Observer<any>;

  constructor(requestObservableFn: () => Observable<any>) {
    this.status = Observable.create((o) => {
      this.loadStatus = o;
    });

    this.reloader = Observable.create((o: Observer<any>) => {
      this.reloadTrigger = o;
    });

    this.value = this.reloader.startWith(null).switchMap(() => {
      if (this.loadStatus) {
        this.loadStatus.next(RestResource.LOADING);
      }
      return requestObservableFn()
        .map((res) => {
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.IDLE);
          }
          return res;
        }).catch((err)=>{
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.ERROR);
          }
          return Observable.of(null);
        });
    }).publishReplay(1).refCount();
  }

  reload() {
    this.reloadTrigger.next(null);
  }

}
like image 131
Matjaz Hirsman Avatar answered Nov 11 '22 12:11

Matjaz Hirsman