import { Observable, Subject, of, throwError } from 'rxjs'; import { tap, filter } from 'rxjs/operators'; interface CacheContent { expiry: number; value: any; } /* Based on this article: https://hackernoon.com/angular-simple-in-memory-cache-service-on-the-ui-with-rxjs-77f167387e39 */ export class CacheService { private cache: Map = new Map(); private inFlightObservables: Map> = new Map>(); readonly DEFAULT_MAX_AGE: number = 300000; /** * Gets the value from cache if the key is provided. * If no value exists in cache, then check if the same call exists * in flight, if so return the subject. If not create a new * Subject inFlightObservable and return the source observable. */ get(key: string, fallback?: Observable, maxAge?: number): Observable | Subject { console.log(`%cIN cache GET 1`, 'color: magenta'); if (this.hasValidCachedValue(key)) { console.log(`%cIN cache GET 2`, 'color: magenta'); const entry = this.cache.get(key); console.log(`%cGetting from cache ${key}: ${JSON.stringify(entry)}`, 'color: green'); return of(entry.value); } if (!maxAge) { maxAge = this.DEFAULT_MAX_AGE; } if (this.inFlightObservables.has(key)) { // console.log(`%cIN cache GET 3`, 'color: magenta'); return this.inFlightObservables.get(key); } else if (fallback && fallback instanceof Observable) { // console.log(`%cIN cache GET 4`, 'color: magenta'); this.inFlightObservables.set(key, new Subject()); return fallback.pipe( // tap((event) => console.log(`%cSETTING IN CACHE 1: ${key} ${JSON.stringify(event)}`, 'color: blue')), filter(event => true), // event instanceof HttpResponse), /* Note: This filters out the strange 'type' responses emitted by a request observable. */ tap((event) => { // console.log(`%cSETTING IN CACHE ${key} ${JSON.stringify(event)}`, 'color: purple'); this.set(key, event, maxAge); }) ) } else { console.log(`%cIN cache GET 5`, 'color: magenta'); return throwError(() => 'Requested key is not available in Cache'); } } /** * Sets the value with key in the cache * Notifies all observers of the new value */ set(key: string, value: any, maxAge: number = this.DEFAULT_MAX_AGE): void { this.cache.set(key, { value: value, expiry: Date.now() + maxAge }); this.notifyInFlightObservers(key, value); } /** * Checks if the a key exists in cache */ has(key: string): boolean { return this.cache.has(key); } /** * Publishes the value to all observers of the given * in progress observables if observers exist. */ private notifyInFlightObservers(key: string, value: any): void { if (this.inFlightObservables.has(key)) { const inFlight = this.inFlightObservables.get(key); const observersCount = inFlight.observers.length; if (observersCount) { inFlight.next(value); } inFlight.complete(); this.inFlightObservables.delete(key); } } /** * Checks if the key exists and has not expired. */ private hasValidCachedValue(key: string): boolean { if (this.cache.has(key)) { const entry = this.cache.get(key); if (entry.expiry < Date.now()) { this.cache.delete(key); return false; } return true; } else { return false; } } }