diff --git a/generators/app/templates/src/app/core/_core.module.ts b/generators/app/templates/src/app/core/_core.module.ts index b8070a39..4960280d 100644 --- a/generators/app/templates/src/app/core/_core.module.ts +++ b/generators/app/templates/src/app/core/_core.module.ts @@ -1,11 +1,10 @@ import { NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { RouteReuseStrategy, RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { RouteReusableStrategy } from './route-reusable-strategy'; -import { HttpService } from './http/http.service'; @NgModule({ imports: [ @@ -16,7 +15,7 @@ import { HttpService } from './http/http.service'; ], providers: [ { - provide: HttpClient, + provide: HTTP_INTERCEPTORS, useClass: HttpService }, { diff --git a/generators/app/templates/src/app/core/_index.ts b/generators/app/templates/src/app/core/_index.ts index 0892e57a..d231ed87 100644 --- a/generators/app/templates/src/app/core/_index.ts +++ b/generators/app/templates/src/app/core/_index.ts @@ -1,9 +1,6 @@ export * from './core.module'; export * from './i18n.service'; -export * from './http/http.service'; -export * from './http/http-cache.service'; export * from './http/api-prefix.interceptor'; -export * from './http/cache.interceptor'; export * from './http/error-handler.interceptor'; export * from './route-reusable-strategy'; export * from './logger.service'; diff --git a/generators/app/templates/src/app/core/http/cache.interceptor.spec.ts b/generators/app/templates/src/app/core/http/cache.interceptor.spec.ts deleted file mode 100644 index 615becbe..00000000 --- a/generators/app/templates/src/app/core/http/cache.interceptor.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Type } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http'; - -import { CacheInterceptor } from './cache.interceptor'; -import { HttpCacheService } from './http-cache.service'; - -describe('CacheInterceptor', () => { - let interceptorOptions: object | null = {}; - let httpCacheService: HttpCacheService; - let http: HttpClient; - let httpMock: HttpTestingController; - - function createInterceptor(_httpCacheService: HttpCacheService) { - return new CacheInterceptor(_httpCacheService).configure(interceptorOptions); - } - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - HttpCacheService, - { - provide: HTTP_INTERCEPTORS, - useFactory: createInterceptor, - deps: [HttpCacheService], - multi: true - } - ] - }); - }); - - afterEach(() => { - httpCacheService.cleanCache(); - httpMock.verify(); - }); - - describe('with default configuration', () => { - beforeEach(() => { - interceptorOptions = null; - http = TestBed.inject(HttpClient); - httpMock = TestBed.inject(HttpTestingController as Type); - httpCacheService = TestBed.inject(HttpCacheService); - }); - - it('should cache the request', () => { - // Act - http.get('/toto').subscribe(() => { - // Assert - const cachedData = httpCacheService.getCacheData('/toto'); - expect(cachedData).toBeDefined(); - expect(cachedData ? cachedData.body : null).toEqual('someData'); - }); - - httpMock.expectOne({ url: '/toto' }).flush('someData'); - }); - - it('should respond from the cache', () => { - // Arrange - httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' })); - - // Act - http.get('/toto').subscribe(response => { - // Assert - expect(response).toEqual('cachedData'); - }); - - httpMock.expectNone({ url: '/toto' }); - }); - - it('should not cache the request in case of error', () => { - // Act - http.get('/toto').subscribe( - () => {}, - () => { - // Assert - expect(httpCacheService.getCacheData('/toto')).toBeNull(); - } - ); - - httpMock.expectOne({}).flush(null, { - status: 404, - statusText: 'error' - }); - }); - }); - - describe('with update forced configuration', () => { - beforeEach(() => { - interceptorOptions = { update: true }; - http = TestBed.inject(HttpClient); - httpMock = TestBed.inject(HttpTestingController as Type); - httpCacheService = TestBed.inject(HttpCacheService); - }); - - afterEach(() => { - httpCacheService.cleanCache(); - httpMock.verify(); - }); - - it('should force cache update', () => { - // Arrange - httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' })); - - // Act - http.get('/toto').subscribe(response => { - // Assert - expect(response).toEqual('newData'); - }); - - httpMock.expectOne({ url: '/toto' }).flush('newData'); - }); - }); -}); diff --git a/generators/app/templates/src/app/core/http/cache.interceptor.ts b/generators/app/templates/src/app/core/http/cache.interceptor.ts deleted file mode 100644 index 7641931b..00000000 --- a/generators/app/templates/src/app/core/http/cache.interceptor.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; -import { Observable, Subscriber } from 'rxjs'; - -import { HttpCacheService } from './http-cache.service'; - -/** - * Caches HTTP requests. - * Use ExtendedHttpClient fluent API to configure caching for each request. - */ -@Injectable({ - providedIn: 'root' -}) -export class CacheInterceptor implements HttpInterceptor { - - private forceUpdate = false; - - constructor(private httpCacheService: HttpCacheService) { } - - /** - * Configures interceptor options - * @param options If update option is enabled, forces request to be made and updates cache entry. - * @return The configured instance. - */ - configure(options?: { update?: boolean } | null): CacheInterceptor { - const instance = new CacheInterceptor(this.httpCacheService); - if (options && options.update) { - instance.forceUpdate = true; - } - return instance; - } - - intercept(request: HttpRequest, next: HttpHandler): Observable> { - if (request.method !== 'GET') { - return next.handle(request); - } - - return new Observable((subscriber: Subscriber>) => { - const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams); - if (cachedData !== null) { - // Create new response to avoid side-effects - subscriber.next(new HttpResponse(cachedData as object)); - subscriber.complete(); - } else { - next.handle(request) - .subscribe( - event => { - if (event instanceof HttpResponse) { - this.httpCacheService.setCacheData(request.urlWithParams, event); - } - subscriber.next(event); - }, - error => subscriber.error(error), - () => subscriber.complete() - ); - } - }); - } - -} diff --git a/generators/app/templates/src/app/core/http/http-cache.service.spec.ts b/generators/app/templates/src/app/core/http/http-cache.service.spec.ts deleted file mode 100644 index 5cc156a8..00000000 --- a/generators/app/templates/src/app/core/http/http-cache.service.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpResponse } from '@angular/common/http'; - -import { HttpCacheService, HttpCacheEntry } from './http-cache.service'; - -const cachePersistenceKey = 'httpCache'; - -describe('HttpCacheService', () => { - let httpCacheService: HttpCacheService; - let response: HttpResponse; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [HttpCacheService] - }); - - // Start fresh - window.sessionStorage.removeItem(cachePersistenceKey); - window.localStorage.removeItem(cachePersistenceKey); - - httpCacheService = TestBed.inject(HttpCacheService); - - response = new HttpResponse({ body: 'data' }); - }); - - afterEach(() => { - httpCacheService.cleanCache(); - }); - - describe('setCacheData', () => { - it('should set cache data', () => { - // Act - httpCacheService.setCacheData('/popo', response); - - // Assert - expect(httpCacheService.getCacheData('/popo')).toEqual(response); - }); - - it('should replace existing data', () => { - // Arrange - const newResponse = new HttpResponse({ body: 'new data' }); - - // Act - httpCacheService.setCacheData('/popo', response); - httpCacheService.setCacheData('/popo', newResponse); - - // Assert - expect(httpCacheService.getCacheData('/popo')).toEqual(newResponse); - }); - - it('should set cache date correctly', () => { - // Act - const date = new Date(123); - httpCacheService.setCacheData('/popo', response, date); - httpCacheService.setCacheData('/hoho', response); - - // Assert - expect((httpCacheService.getHttpCacheEntry('/popo') as HttpCacheEntry).lastUpdated).toBe(date); - expect((httpCacheService.getHttpCacheEntry('/hoho') as HttpCacheEntry).lastUpdated).not.toBe(date); - }); - }); - - describe('getCacheData', () => { - it('should return null if no cache', () => { - expect(httpCacheService.getCacheData('/hoho')).toBe(null); - }); - - it('should return cached data if exists', () => { - // Act - httpCacheService.setCacheData('/hoho', response); - - // Assert - expect(httpCacheService.getCacheData('/hoho')).toEqual(response); - }); - - it('should return cached data with url parameters if exists', () => { - // Act - httpCacheService.setCacheData('/hoho?pif=paf', response); - - // Assert - expect(httpCacheService.getCacheData('/hoho?pif=paf')).toEqual(response); - }); - }); - - describe('getHttpCacheEntry', () => { - it('should return null if no cache', () => { - expect(httpCacheService.getHttpCacheEntry('/hoho')).toBe(null); - }); - - it('should return cached data date if exists', () => { - // Arrange - const date = new Date(123); - - // Act - httpCacheService.setCacheData('/hoho', response, date); - const entry = httpCacheService.getHttpCacheEntry('/hoho') as HttpCacheEntry; - - // Assert - expect(entry).not.toBeNull(); - expect(entry.lastUpdated).toEqual(date); - expect(entry.data).toEqual(response); - }); - }); - - describe('clearCacheData', () => { - it('should clear existing cache data', () => { - // Set cache - httpCacheService.setCacheData('/hoho', response); - expect(httpCacheService.getCacheData('/hoho')).toEqual(response); - - // Clear cache - httpCacheService.clearCache('/hoho'); - expect(httpCacheService.getCacheData('/hoho')).toBe(null); - }); - - it('should do nothing if no cache exists', () => { - expect(httpCacheService.getCacheData('/lolo')).toBe(null); - httpCacheService.clearCache('/hoho'); - expect(httpCacheService.getCacheData('/lolo')).toBe(null); - }); - }); - - describe('cleanCache', () => { - it('should clear all cache if no date is specified', () => { - // Set cache - httpCacheService.setCacheData('/hoho', response); - httpCacheService.setCacheData('/popo', response); - expect(httpCacheService.getCacheData('/hoho')).toBe(response); - expect(httpCacheService.getCacheData('/popo')).toBe(response); - - // Clean cache - httpCacheService.cleanCache(); - expect(httpCacheService.getCacheData('/hoho')).toBe(null); - expect(httpCacheService.getCacheData('/popo')).toBe(null); - }); - - it('should clear existing since specified date', () => { - // Set cache - httpCacheService.setCacheData('/hoho', response); - expect(httpCacheService.getCacheData('/hoho')).toBe(response); - - // Clean cache - httpCacheService.cleanCache(new Date()); - expect(httpCacheService.getCacheData('/hoho')).toBe(null); - }); - - it('should not affect cache entries newer than specified date', () => { - // Set cache - httpCacheService.setCacheData('/hoho', response); - expect(httpCacheService.getCacheData('/hoho')).toBe(response); - - // Clean cache - const date = new Date(); - httpCacheService.setCacheData('/lolo', response, new Date(date.getTime() + 10)); - httpCacheService.cleanCache(date); - - // Assert - expect(httpCacheService.getCacheData('/hoho')).toBe(null); - expect(httpCacheService.getCacheData('/lolo')).toBe(response); - }); - }); - - describe('setPersistence', () => { - beforeEach(() => { - httpCacheService.setPersistence(); - httpCacheService.cleanCache = jasmine.createSpy('cleanCache'); - }); - - it('should clear previous cache data when persistence value change', () => { - httpCacheService.setPersistence('local'); - expect(httpCacheService.cleanCache).toHaveBeenCalledWith(); - }); - - it('should persist cache to local storage', () => { - expect(localStorage.getItem(cachePersistenceKey)).toBeNull(); - - httpCacheService.setPersistence('local'); - httpCacheService.setCacheData('/hoho', response); - - expect(localStorage.getItem(cachePersistenceKey)).not.toBeNull(); - }); - - it('should persist cache to session storage', () => { - expect(sessionStorage.getItem(cachePersistenceKey)).toBeNull(); - - httpCacheService.setPersistence('session'); - httpCacheService.setCacheData('/hoho', response); - - expect(sessionStorage.getItem(cachePersistenceKey)).not.toBeNull(); - }); - }); -}); diff --git a/generators/app/templates/src/app/core/http/http-cache.service.ts b/generators/app/templates/src/app/core/http/http-cache.service.ts deleted file mode 100644 index 6867cafc..00000000 --- a/generators/app/templates/src/app/core/http/http-cache.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpResponse } from '@angular/common/http'; - -import { Logger } from '../logger.service'; - -const log = new Logger('HttpCacheService'); -const cachePersistenceKey = 'httpCache'; - -export interface HttpCacheEntry { - lastUpdated: Date; - data: HttpResponse; -} - -/** - * Provides a cache facility for HTTP requests with configurable persistence policy. - */ -@Injectable({ - providedIn: 'root' -}) -export class HttpCacheService { - - private cachedData: { [key: string]: HttpCacheEntry; } = {}; - private storage: Storage | null = null; - - constructor() { - this.loadCacheData(); - } - - /** - * Sets the cache data for the specified request. - * @param url The request URL. - * @param data The received data. - * @param lastUpdated The cache last update, current date is used if not specified. - */ - setCacheData(url: string, data: HttpResponse, lastUpdated?: Date) { - this.cachedData[url] = { - lastUpdated: lastUpdated || new Date(), - data - }; - log.debug(`Cache set for key: "${url}"`); - this.saveCacheData(); - } - - /** - * Gets the cached data for the specified request. - * @param url The request URL. - * @return The cached data or null if no cached data exists for this request. - */ - getCacheData(url: string): HttpResponse | null { - const cacheEntry = this.cachedData[url]; - - if (cacheEntry) { - log.debug(`Cache hit for key: "${url}"`); - return cacheEntry.data; - } - - return null; - } - - /** - * Gets the cached entry for the specified request. - * @param url The request URL. - * @return The cache entry or null if no cache entry exists for this request. - */ - getHttpCacheEntry(url: string): HttpCacheEntry | null { - return this.cachedData[url] || null; - } - - /** - * Clears the cached entry (if exists) for the specified request. - * @param url The request URL. - */ - clearCache(url: string): void { - delete this.cachedData[url]; - log.debug(`Cache cleared for key: "${url}"`); - this.saveCacheData(); - } - - /** - * Cleans cache entries older than the specified date. - * @param expirationDate The cache expiration date. If no date is specified, all cache is cleared. - */ - cleanCache(expirationDate?: Date) { - if (expirationDate) { - Object.entries(this.cachedData).forEach(([key, value]) => { - if (expirationDate >= value.lastUpdated) { - delete this.cachedData[key]; - } - }); - } else { - this.cachedData = {}; - } - this.saveCacheData(); - } - - /** - * Sets the cache persistence policy. - * Note that changing the cache persistence will also clear the cache from its previous storage. - * @param persistence How the cache should be persisted, it can be either local or session storage, or if no value is - * provided it will be only in-memory (default). - */ - setPersistence(persistence?: 'local' | 'session') { - this.cleanCache(); - this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null; - this.loadCacheData(); - } - - private saveCacheData() { - if (this.storage) { - this.storage.setItem(cachePersistenceKey, JSON.stringify(this.cachedData)); - } - } - - private loadCacheData() { - const data = this.storage ? this.storage.getItem(cachePersistenceKey) : null; - this.cachedData = data ? JSON.parse(data) : {}; - } - -} diff --git a/generators/app/templates/src/app/core/http/http.service.spec.ts b/generators/app/templates/src/app/core/http/http.service.spec.ts deleted file mode 100644 index 12680ef4..00000000 --- a/generators/app/templates/src/app/core/http/http.service.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Type } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { HttpClient, HttpInterceptor } from '@angular/common/http'; - -import { HttpService } from './http.service'; -import { HttpCacheService } from './http-cache.service'; -import { ErrorHandlerInterceptor } from './error-handler.interceptor'; -import { CacheInterceptor } from './cache.interceptor'; -import { ApiPrefixInterceptor } from './api-prefix.interceptor'; - -describe('HttpService', () => { - let httpCacheService: HttpCacheService; - let http: HttpClient; - let httpMock: HttpTestingController; - let interceptors: HttpInterceptor[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - ErrorHandlerInterceptor, - CacheInterceptor, - ApiPrefixInterceptor, - HttpCacheService, - { - provide: HttpClient, - useClass: HttpService - } - ] - }); - - http = TestBed.inject(HttpClient); - httpMock = TestBed.inject(HttpTestingController as Type); - httpCacheService = TestBed.inject(HttpCacheService); - - const realRequest = http.request; - spyOn(HttpService.prototype, 'request').and.callFake( - function(this: any, method: string, url: string, options?: any) { - interceptors = this.interceptors; - return realRequest.call(this, method, url, options); - } - ); - }); - - afterEach(() => { - httpCacheService.cleanCache(); - httpMock.verify(); - }); - - it('should use error handler, API prefix and no cache by default', () => { - // Act - const request = http.get('/toto'); - - // Assert - request.subscribe(() => { - expect(http.request).toHaveBeenCalled(); - expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); - }); - httpMock.expectOne({}).flush({}); - }); - - it('should use cache', () => { - // Act - const request = http.cache().get('/toto'); - - // Assert - request.subscribe(() => { - expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy(); - }); - httpMock.expectOne({}).flush({}); - }); - - it('should skip error handler', () => { - // Act - const request = http.skipErrorHandler().get('/toto'); - - // Assert - request.subscribe(() => { - expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy(); - expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); - }); - httpMock.expectOne({}).flush({}); - }); - - it('should not use API prefix', () => { - // Act - const request = http.disableApiPrefix().get('/toto'); - - // Assert - request.subscribe(() => { - expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeFalsy(); - expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); - expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); - }); - httpMock.expectOne({}).flush({}); - }); -}); diff --git a/generators/app/templates/src/app/core/http/http.service.ts b/generators/app/templates/src/app/core/http/http.service.ts deleted file mode 100644 index a34daeb6..00000000 --- a/generators/app/templates/src/app/core/http/http.service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Inject, Injectable, InjectionToken, Injector, Optional, Type } from '@angular/core'; -import { HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; -import { Observable } from 'rxjs'; - -import { ErrorHandlerInterceptor } from './error-handler.interceptor'; -import { CacheInterceptor } from './cache.interceptor'; -import { ApiPrefixInterceptor } from './api-prefix.interceptor'; - -// HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly -// (see https://github.com/Microsoft/TypeScript/issues/13897) -declare module '@angular/common/http/http' { - - // Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of - // HttpClient with HttpService using dependency injection - export interface HttpClient { - - /** - * Enables caching for this request. - * @param forceUpdate Forces request to be made and updates cache entry. - * @return The new instance. - */ - cache(forceUpdate?: boolean): HttpClient; - - /** - * Skips default error handler for this request. - * @return The new instance. - */ - skipErrorHandler(): HttpClient; - - /** - * Do not use API prefix for this request. - * @return The new instance. - */ - disableApiPrefix(): HttpClient; - - } - -} - -// From @angular/common/http/src/interceptor: allows to chain interceptors -class HttpInterceptorHandler implements HttpHandler { - - constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { } - - handle(request: HttpRequest): Observable> { - return this.interceptor.intercept(request, this.next); - } - -} - -/** - * Allows to override default dynamic interceptors that can be disabled with the HttpService extension. - * Except for very specific needs, you should better configure these interceptors directly in the constructor below - * for better readability. - * - * For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard - * HTTP_INTERCEPTORS token. - */ -export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken('HTTP_DYNAMIC_INTERCEPTORS'); - -/** - * Extends HttpClient with per request configuration using dynamic interceptors. - */ -@Injectable({ - providedIn: 'root' -}) -export class HttpService extends HttpClient { - - constructor(private httpHandler: HttpHandler, - private injector: Injector, - @Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = []) { - super(httpHandler); - - if (!this.interceptors) { - // Configure default interceptors that can be disabled here - this.interceptors = [ - this.injector.get(ApiPrefixInterceptor), - this.injector.get(ErrorHandlerInterceptor) - ]; - } - } - - cache(forceUpdate?: boolean): HttpClient { - const cacheInterceptor = this.injector.get(CacheInterceptor as Type) - .configure({ update: forceUpdate }); - return this.addInterceptor(cacheInterceptor); - } - - skipErrorHandler(): HttpClient { - return this.removeInterceptor(ErrorHandlerInterceptor); - } - - disableApiPrefix(): HttpClient { - return this.removeInterceptor(ApiPrefixInterceptor); - } - - // Override the original method to wire interceptors when triggering the request. - request(method?: any, url?: any, options?: any): any { - const handler = this.interceptors.reduceRight( - (next, interceptor) => new HttpInterceptorHandler(next, interceptor), - this.httpHandler - ); - return new HttpClient(handler).request(method, url, options); - } - - private removeInterceptor(interceptorType: Type): HttpService { - return new HttpService( - this.httpHandler, - this.injector, - this.interceptors.filter(i => !(i instanceof interceptorType)) - ); - } - - private addInterceptor(interceptor: HttpInterceptor): HttpService { - return new HttpService( - this.httpHandler, - this.injector, - this.interceptors.concat([interceptor]) - ); - } - -} diff --git a/generators/app/templates/src/app/home/quote.service.ts b/generators/app/templates/src/app/home/quote.service.ts index 39c7554f..3a593208 100644 --- a/generators/app/templates/src/app/home/quote.service.ts +++ b/generators/app/templates/src/app/home/quote.service.ts @@ -21,7 +21,6 @@ export class QuoteService { getRandomQuote(context: RandomQuoteContext): Observable { return this.httpClient - .cache() .get(routes.quote(context)) .pipe( map((body: any) => body.value),