From 9589f25e57ba95b9741b0b403c3c63fad38b251c Mon Sep 17 00:00:00 2001 From: Jesse Lucas Date: Tue, 24 Mar 2020 19:19:54 -0400 Subject: [PATCH] refactor to use caching interceptor --- src/app/app.module.ts | 1 - .../folder-chart/folder-chart.component.ts | 1 - src/app/db-status.service.ts | 12 +--- .../caching.interceptor.spec.ts | 16 +++++ .../http-interceptors/caching.interceptor.ts | 58 +++++++++++++++++++ src/app/http-interceptors/csrf-intercepor.ts | 29 ---------- .../csrf.interceptor.spec.ts | 16 +++++ src/app/http-interceptors/csrf.interceptor.ts | 29 ++++++++++ src/app/http-interceptors/index.ts | 6 +- .../device-list/device-list.component.ts | 1 - src/app/request-cache.service.spec.ts | 16 +++++ src/app/request-cache.service.ts | 50 ++++++++++++++++ src/app/system-config.service.ts | 1 + 13 files changed, 192 insertions(+), 44 deletions(-) create mode 100644 src/app/http-interceptors/caching.interceptor.spec.ts create mode 100644 src/app/http-interceptors/caching.interceptor.ts delete mode 100644 src/app/http-interceptors/csrf-intercepor.ts create mode 100644 src/app/http-interceptors/csrf.interceptor.spec.ts create mode 100644 src/app/http-interceptors/csrf.interceptor.ts create mode 100644 src/app/request-cache.service.spec.ts create mode 100644 src/app/request-cache.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8a454f47a..7ac406714 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -29,7 +29,6 @@ import { InMemoryConfigDataService } from './in-memory-config-data.service'; import { deviceID } from './api-utils'; import { environment } from '../environments/environment'; import { ChartItemComponent } from './charts/chart-item/chart-item.component'; -import { CSRFInterceptor } from './http-interceptors/csrf-intercepor'; @NgModule({ declarations: [ diff --git a/src/app/charts/folder-chart/folder-chart.component.ts b/src/app/charts/folder-chart/folder-chart.component.ts index 98127336a..039fd5170 100644 --- a/src/app/charts/folder-chart/folder-chart.component.ts +++ b/src/app/charts/folder-chart/folder-chart.component.ts @@ -38,7 +38,6 @@ export class FolderChartComponent implements OnInit { if (s.label === state) { s.count = s.count + 1; found = true; - console.log("increase count", s.count); } }); diff --git a/src/app/db-status.service.ts b/src/app/db-status.service.ts index 4b7474b37..b1e2151ce 100644 --- a/src/app/db-status.service.ts +++ b/src/app/db-status.service.ts @@ -14,18 +14,10 @@ import Folder from './folder' }) export class DbStatusService { private dbStatusUrl = environment.production ? apiURL + 'rest/db/status' : 'api/dbStatus'; - private statuses: Map; - constructor(private http: HttpClient, private cookieService: CookieService) { - this.statuses = new Map(); - } + constructor(private http: HttpClient, private cookieService: CookieService) { } getFolderStatus(id: string): Observable { - // First check to see if we have a cached value - if (this.statuses.has(id)) { - return of(this.statuses.get(id)); - } - let httpOptions: { params: HttpParams }; if (id) { httpOptions = { @@ -46,8 +38,6 @@ export class DbStatusService { res = res[0]; } } - // cache result - this.statuses.set(id, res) return res; }) ); diff --git a/src/app/http-interceptors/caching.interceptor.spec.ts b/src/app/http-interceptors/caching.interceptor.spec.ts new file mode 100644 index 000000000..ee914ec93 --- /dev/null +++ b/src/app/http-interceptors/caching.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CachingInterceptor } from './caching.interceptor'; + +describe('CachingInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + CachingInterceptor + ] + })); + + it('should be created', () => { + const interceptor: CachingInterceptor = TestBed.inject(CachingInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/http-interceptors/caching.interceptor.ts b/src/app/http-interceptors/caching.interceptor.ts new file mode 100644 index 000000000..f610d6eb8 --- /dev/null +++ b/src/app/http-interceptors/caching.interceptor.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, + HttpHeaders, + HttpResponse +} from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { RequestCacheService } from '../request-cache.service' + +@Injectable() +export class CachingInterceptor implements HttpInterceptor { + constructor(private cache: RequestCacheService) { } + + intercept(req: HttpRequest, next: HttpHandler) { + // continue if not cachable. + if (!isCachable(req)) { return next.handle(req); } + + const cachedResponse = this.cache.get(req); + return cachedResponse ? + of(cachedResponse) : sendRequest(req, next, this.cache); + } +} + +/** Is this request cachable? */ +function isCachable(req: HttpRequest) { + // Only GET requests are cachable + return req.method === 'GET'; + /* + return req.method === 'GET' && + -1 < req.url.indexOf("url"); + */ +} + +/** + * Get server response observable by sending request to `next()`. + * Will add the response to the cache on the way out. + */ +function sendRequest( + req: HttpRequest, + next: HttpHandler, + cache: RequestCacheService): Observable> { + + // No headers allowed in npm search request + const noHeaderReq = req.clone({ headers: new HttpHeaders() }); + + return next.handle(noHeaderReq).pipe( + tap(event => { + // There may be other events besides the response. + if (event instanceof HttpResponse) { + // cache.put(req, event); // Update the cache. + } + }) + ); +} \ No newline at end of file diff --git a/src/app/http-interceptors/csrf-intercepor.ts b/src/app/http-interceptors/csrf-intercepor.ts deleted file mode 100644 index a76cb0146..000000000 --- a/src/app/http-interceptors/csrf-intercepor.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { deviceID } from '../api-utils'; -import { - HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders -} from '@angular/common/http'; - -import { CookieService } from '../cookie.service'; - - -@Injectable() -export class CSRFInterceptor implements HttpInterceptor { - - constructor(private cookieService: CookieService) { } - - intercept(req: HttpRequest, next: HttpHandler) { - const dID: String = deviceID(); - const csrfCookie = 'CSRF-Token-' + dID - - // Clone the request and replace the original headers with - // cloned headers, updated with the CSRF information. - const csrfReq = req.clone({ - headers: req.headers.set('X-CSRF-Token-' + dID, - this.cookieService.getCookie(csrfCookie)) - }); - - // send cloned request with header to the next handler. - return next.handle(csrfReq); - } -} \ No newline at end of file diff --git a/src/app/http-interceptors/csrf.interceptor.spec.ts b/src/app/http-interceptors/csrf.interceptor.spec.ts new file mode 100644 index 000000000..a8f998869 --- /dev/null +++ b/src/app/http-interceptors/csrf.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CSRFInterceptor } from './csrf.interceptor'; + +describe('CsrfInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + CSRFInterceptor + ] + })); + + it('should be created', () => { + const interceptor: CSRFInterceptor = TestBed.inject(CSRFInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/http-interceptors/csrf.interceptor.ts b/src/app/http-interceptors/csrf.interceptor.ts new file mode 100644 index 000000000..70cc07e9e --- /dev/null +++ b/src/app/http-interceptors/csrf.interceptor.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { deviceID } from '../api-utils'; +import { + HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders +} from '@angular/common/http'; + +import { CookieService } from '../cookie.service'; + + +@Injectable() +export class CSRFInterceptor implements HttpInterceptor { + + constructor(private cookieService: CookieService) { } + + intercept(req: HttpRequest, next: HttpHandler) { + const dID: String = deviceID(); + const csrfCookie = 'CSRF-Token-' + dID + + // Clone the request and replace the original headers with + // cloned headers, updated with the CSRF information. + const csrfReq = req.clone({ + headers: req.headers.set('X-CSRF-Token-' + dID, + this.cookieService.getCookie(csrfCookie)) + }); + + // send cloned request with header to the next handler. + return next.handle(csrfReq); + } +} \ No newline at end of file diff --git a/src/app/http-interceptors/index.ts b/src/app/http-interceptors/index.ts index 3056fbff4..53635a917 100644 --- a/src/app/http-interceptors/index.ts +++ b/src/app/http-interceptors/index.ts @@ -1,8 +1,12 @@ /* "Barrel" of Http Interceptors */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { CSRFInterceptor } from './csrf-intercepor'; + +import { CSRFInterceptor } from './csrf.interceptor'; +import { CachingInterceptor } from './caching.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }, + // CSRFInterceptor needs to be last { provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true }, ]; \ No newline at end of file diff --git a/src/app/lists/device-list/device-list.component.ts b/src/app/lists/device-list/device-list.component.ts index 63e971f5e..d2aaabac8 100644 --- a/src/app/lists/device-list/device-list.component.ts +++ b/src/app/lists/device-list/device-list.component.ts @@ -34,7 +34,6 @@ export class DeviceListComponent implements AfterViewInit, OnInit { this.systemConfigService.getDevices().subscribe( data => { - console.log("get data??", data) this.dataSource.data = data; this.dataSource.dataSubject.next(data); } diff --git a/src/app/request-cache.service.spec.ts b/src/app/request-cache.service.spec.ts new file mode 100644 index 000000000..4d65caa17 --- /dev/null +++ b/src/app/request-cache.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { RequestCacheService } from './request-cache.service'; + +describe('RequestCacheService', () => { + let service: RequestCacheService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RequestCacheService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/request-cache.service.ts b/src/app/request-cache.service.ts new file mode 100644 index 000000000..2396378b6 --- /dev/null +++ b/src/app/request-cache.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { HttpResponse, HttpRequest } from '@angular/common/http'; + +export interface RequestCacheEntry { + url: string; + response: HttpResponse; + lastRead: number; +} + +const maxAge = 30000; // milliseconds + +@Injectable({ + providedIn: 'root' +}) +export class RequestCacheService { + private cache: Map = new Map(); + + constructor() { } + + get(req: HttpRequest): HttpResponse | undefined { + const url = req.urlWithParams; + const cached = this.cache.get(url); + + if (!cached) { + return undefined; + } + + const isExpired = cached.lastRead < (Date.now() - maxAge); + return isExpired ? undefined : cached.response; + } + + put(req: HttpRequest, response: HttpResponse): void { + const url = req.urlWithParams; + + const entry = { url, response, lastRead: Date.now() }; + this.cache.set(url, entry); + + // Remove expired cache entries + const expired = Date.now() - maxAge; + this.cache.forEach(entry => { + if (entry.lastRead < expired) { + this.cache.delete(entry.url); + } + }); + } + + clearAll(): void { + this.cache = new Map(); + } +} diff --git a/src/app/system-config.service.ts b/src/app/system-config.service.ts index 8a9dd57d8..d045d95de 100644 --- a/src/app/system-config.service.ts +++ b/src/app/system-config.service.ts @@ -60,6 +60,7 @@ export class SystemConfigService { return folderObservable; } + // TODO switch to devices getDevices(): Observable { const deviceObserverable: Observable = new Observable((observer) => { if (this.folders) {