refactor to use caching interceptor

This commit is contained in:
Jesse Lucas 2020-03-24 19:19:54 -04:00
parent 15c2556e06
commit 9589f25e57
13 changed files with 192 additions and 44 deletions

View File

@ -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: [

View File

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

View File

@ -14,18 +14,10 @@ import Folder from './folder'
})
export class DbStatusService {
private dbStatusUrl = environment.production ? apiURL + 'rest/db/status' : 'api/dbStatus';
private statuses: Map<string, Folder.Status>;
constructor(private http: HttpClient, private cookieService: CookieService) {
this.statuses = new Map();
}
constructor(private http: HttpClient, private cookieService: CookieService) { }
getFolderStatus(id: string): Observable<Folder.Status> {
// 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;
})
);

View File

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

View File

@ -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<any>, 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<any>) {
// 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<any>,
next: HttpHandler,
cache: RequestCacheService): Observable<HttpEvent<any>> {
// 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.
}
})
);
}

View File

@ -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<any>, 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);
}
}

View File

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

View File

@ -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<any>, 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);
}
}

View File

@ -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 },
];

View File

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

View File

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

View File

@ -0,0 +1,50 @@
import { Injectable } from '@angular/core';
import { HttpResponse, HttpRequest } from '@angular/common/http';
export interface RequestCacheEntry {
url: string;
response: HttpResponse<any>;
lastRead: number;
}
const maxAge = 30000; // milliseconds
@Injectable({
providedIn: 'root'
})
export class RequestCacheService {
private cache: Map<string, RequestCacheEntry> = new Map();
constructor() { }
get(req: HttpRequest<any>): HttpResponse<any> | 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<any>, response: HttpResponse<any>): 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();
}
}

View File

@ -60,6 +60,7 @@ export class SystemConfigService {
return folderObservable;
}
// TODO switch to devices
getDevices(): Observable<Device[]> {
const deviceObserverable: Observable<Device[]> = new Observable((observer) => {
if (this.folders) {