Peter G. Horvath
Peter G. Horvath

Reputation: 545

Wrapping Angular 4 Observable HTTP calls into one Observable for caching

I have an Angular 4 application, that acts as a dashboard of a system. A number of different components call the same backing REST call through the same TypeScript service classes. While this works, I would like to avoid the unnecessary duplicated request storms hammering the server by introducing some caching service on the client side.

I have implemented something like this for my caching (in TypeScript), which is then used by my services, that pass the HTTP call in as computeFunction:

@Injectable()
export class CacheService {

  private cacheMap = {};


  getAsObservable<V>(
                key: string,
                expirationThresholdSeconds: number,
                computeFunction: () => Observable<V>): Observable<V> {

    const cacheEntry = this.cacheMap[key];

    if (...) {
      // if cached entry is valid, return it immediately

      return Observable.of<V>(cacheEntry.value);          
    } else {
      // if not found or expired, call the method, and use map(...) to store the returned value
      return computeFunction().map(returnValue => {

        const expirationTime = new Date().getTime() + (expirationThresholdSeconds * 1000);

        const newCacheEntry = ... // build cache entry with expiration set

        this.cacheMap[key] = newCacheEntry;

        return returnValue;
    });
  }

}

This works correctly, however, if calls with the same key are made in quick succession (e.g. when the application is starting), they will all fired against the server, as the cache does not have the return value at the time of the check.

So I think I should instead somehow implement my own cacheable wrapper "multiplexing" Observable, that can be returned to multiple cache callers, that

  1. Executes the call passed in computeFunction only once
  2. Caches the return value
  3. Returns the value to each of its subscribers, then cleans up itself like the HTTP Observables do, so that you don't have to unsubscribe().

Could someone please give me a sample on how to do this?

The challenge is that the Observable should handle both cases, when

Or am heading into the wrong direction and getting the whole thing overcomplicated? If there is a simpler concept I could follow, I would be more grateful to learn it.

Upvotes: 0

Views: 940

Answers (1)

Ingo B&#252;rk
Ingo B&#252;rk

Reputation: 20063

You don't need a lot of fancy logic. You can just use shareReplay(1) to multicast the observable. Here's an example:

// Simulate an async call
// Side effect logging to track when our API call is actually made
const api$ = Observable.of('Hello, World!').delay(1000)
    .do(() => console.log('API called!'));

const source$ = api$
     // We have to make sure that the observable doesn't complete, otherwise
     // shareReplay() will reconnect if the ref count goes to zero
     // in the mean time. You can leave this out if you do actually
     // want to "invalidate the cache" if at some point all observers
     // have unsubscribed.
    .concat(Observable.never())
     // Let the magic happen!
    .shareReplay(1);

Now you can subscribe all you want:

// Two parallel subscriptions
const sub1 = source$.subscribe();
const sub2 = source$.subscribe();

// A new subscription when ref count is > 0
sub1.unsubscribe();
const sub3 = source$.subscribe();

// A new subscription after ref count went to 0
sub2.unsubscribe();
sub3.unsubscribe();
const sub4 = source$.subscribe();
sub4.unsubscribe();

And all you will see is a single log statement.

If you want a time-based expiration, you can get rid of the never() and instead do this:

const source$ = Observable.timer(0, expirationTimeout)
    .switchMap(() => api$)
    .shareReplay(1);

Note, though, that this is a hot stream that will query the API until all observers unsubscribe – so beware of memory leaks.


On a small note, the Observable.never() trick will only work on very recent versions of rxjs due to this fixed bug. The same goes for the timer-based solution.

Upvotes: 1

Related Questions