5

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?

1

3 Answers 3

2

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);
  }

}
Sign up to request clarification or add additional context in comments.

Comments

0

With option 3. to allow clearing of the cache you could assign the observable to a private member and return that, eg.

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

clearCustomerCache() {
    this._customers = null;
}

3 Comments

I found this whole approach here and tried setting the observable to null. Unfortunately this didn't work for me - the service continued to return the previous value not making any real http calls. Can you please explain how these actions are supposed to solve the problem (what's the idea)?
``` Property 'publishLast' does not exist on type 'Observable<any>'.``` How can i solve this error ?
@Vivek try - import 'rxjs/add/operator/publishLast';
0

My approach to caching would be keeping a state in a reducer/scan fn:

edit 3: Added a piece of code to invalidate the cache by keyboard event.

edit 2: The windowWhen operator is also suitable for the task and allows to express the logic in a pretty concise way:

const Rx = require('rxjs/Rx');
const process = require('process');
const stdin = process.stdin;

// ceremony to have keypress events in node

stdin.setRawMode(true);
stdin.setEncoding('utf8');
stdin.resume();

// split the keypress observable into ctrl-c and c observables.

const keyPressed$ = Rx.Observable.fromEvent(stdin, 'data').share();
const ctrlCPressed$ = keyPressed$.filter(code => code === '\u0003');
const cPressed$ = keyPressed$.filter(code => code === 'c');

ctrlCPressed$.subscribe(() => process.exit());

function asyncOp() {
  return Promise.resolve(Date().toString());
}

const invalidateCache$ = Rx.Observable.interval(5000).merge(cPressed$);
const interval$ = Rx.Observable.interval(1000);

interval$
  .windowWhen(() => invalidateCache$)
  .map(win => win.mergeScan((acc, value) => {
    if (acc === undefined) {
      const promise = asyncOp();
      return Rx.Observable.from(promise);
    }

    return Rx.Observable.of(acc);
  }, undefined))
  .mergeAll()
  .subscribe(console.log);

It would perform the async option only every 5s and cache the result for other omissions on the observable.

Sun Apr 16 2017 11:24:53 GMT+0200 (CEST)
Sun Apr 16 2017 11:24:53 GMT+0200 (CEST)
Sun Apr 16 2017 11:24:53 GMT+0200 (CEST)
Sun Apr 16 2017 11:24:53 GMT+0200 (CEST)
Sun Apr 16 2017 11:24:57 GMT+0200 (CEST)
Sun Apr 16 2017 11:24:57 GMT+0200 (CEST)

2 Comments

Your answer is very thorough, thanks! One thing however: if I was going to clear the cache based on a timeout (as far as I could understand) I would rather use publishReplay(1, 1000) instead of publishLast() which would invalidate the cache every 1 second. Is this what you intended to show? If yes then it's not really what I wanted - the cache should be cleared explicitly by calling a method. Can you rewrite your example so that the cache will be cleared on demand (with no timeouts logic)?
@Ultimacho ah, i see. the interval in clearCacheInterval$ is meant to be an example, it could be merged/replaced with any other Observable, for example mouse clicks. I changed the example to also clear the cache when the user pressed 'c'.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.