Created
July 10, 2023 13:23
-
-
Save erikunha/d079c31073d556a97d9bbf4e22c2eaa3 to your computer and use it in GitHub Desktop.
Angular cache interceptor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
HttpEvent, | |
HttpHandler, | |
HttpHeaders, | |
HttpRequest, | |
} from '@angular/common/http'; | |
import { | |
HttpClientTestingModule, | |
HttpTestingController, | |
} from '@angular/common/http/testing'; | |
import { TestBed } from '@angular/core/testing'; | |
import { Observable } from 'rxjs'; | |
import { CacheInterceptor } from './cache.interceptor'; | |
describe('CacheInterceptor', () => { | |
let interceptor: CacheInterceptor; | |
let httpMock: HttpTestingController; | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [HttpClientTestingModule], | |
providers: [CacheInterceptor], | |
}); | |
interceptor = TestBed.inject(CacheInterceptor); | |
httpMock = TestBed.inject(HttpTestingController); | |
}); | |
afterEach(() => { | |
httpMock.verify(); | |
}); | |
function createRequest(shouldCache: boolean): HttpRequest<any> { | |
const headers = shouldCache | |
? new HttpHeaders() | |
: new HttpHeaders({ 'Cache-Expiration': 'no-cache' }); | |
return new HttpRequest<any>('GET', 'https://test.com/api/test', { | |
headers, | |
}); | |
} | |
it('should be created', () => { | |
expect(interceptor).toBeTruthy(); | |
}); | |
it('should not cache the response if Cache-Expiration header is set to no-cache', () => { | |
const request = createRequest(false); | |
const next: HttpHandler = { | |
handle: jest.fn().mockReturnValue(new Observable<HttpEvent<any>>()), | |
}; | |
interceptor.intercept(request, next); | |
expect(interceptor.getCachedResponse(request)).toBeNull(); | |
}); | |
it('should cache the response if Cache-Expiration header is not set to no-cache', (done) => { | |
const request = createRequest(true); | |
const next: HttpHandler = { | |
handle: jest.fn().mockReturnValue( | |
new Observable<HttpEvent<any>>((subscriber) => { | |
subscriber.next({ type: 0 }); | |
subscriber.complete(); | |
}) | |
), | |
}; | |
interceptor.intercept(request, next).subscribe(() => { | |
expect(interceptor.getCachedResponse(request)).toBeTruthy(); | |
done(); | |
}); | |
}); | |
it('should return cached response if not expired', (done) => { | |
const request = createRequest(true); | |
const next: HttpHandler = { | |
handle: jest.fn().mockReturnValue( | |
new Observable<HttpEvent<any>>((subscriber) => { | |
subscriber.next({ type: 0 }); | |
subscriber.complete(); | |
}) | |
), | |
}; | |
interceptor.intercept(request, next).subscribe(() => { | |
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 20000); | |
expect(interceptor.getCachedResponse(request)).toBeTruthy(); | |
done(); | |
}); | |
}); | |
it('should not return cached response if expired', (done) => { | |
const request = createRequest(true); | |
const next: HttpHandler = { | |
handle: jest.fn().mockReturnValue( | |
new Observable<HttpEvent<any>>((subscriber) => { | |
subscriber.next({ type: 0 }); | |
subscriber.complete(); | |
}) | |
), | |
}; | |
interceptor.intercept(request, next).subscribe(() => { | |
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 31000); | |
expect(interceptor.getCachedResponse(request)).toBeNull(); | |
done(); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
import { | |
HttpEvent, | |
HttpHandler, | |
HttpInterceptor, | |
HttpRequest, | |
} from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { Observable } from 'rxjs'; | |
import { shareReplay } from 'rxjs/operators'; | |
@Injectable() | |
export class CacheInterceptor implements HttpInterceptor { | |
private cache: Map< | |
string, | |
{ timestamp: number; response$: Observable<HttpEvent<any>> } | |
> = new Map(); | |
intercept( | |
request: HttpRequest<any>, | |
next: HttpHandler | |
): Observable<HttpEvent<any>> { | |
const shouldCache = request.headers.get('Cache-Expiration') !== 'no-cache'; | |
if (shouldCache) { | |
const cachedResponse$ = this.getCachedResponse(request); | |
if (cachedResponse$) { | |
return cachedResponse$; | |
} | |
} | |
const response$ = next.handle(request).pipe(shareReplay()); | |
if (shouldCache) { | |
this.cache.set(request.urlWithParams, { | |
timestamp: Date.now(), | |
response$, | |
}); | |
} | |
return response$; | |
} | |
getCachedResponse( | |
request: HttpRequest<unknown> | |
): Observable<HttpEvent<unknown>> | null { | |
const cached = this.cache.get(request.urlWithParams); | |
if (!cached) { | |
return null; | |
} | |
console.warn('cached', cached); | |
const cacheAge = Date.now() - cached.timestamp; | |
const maxCacheAge = 30000; | |
if (cacheAge > maxCacheAge) { | |
this.cache.delete(request.urlWithParams); | |
return null; | |
} | |
return cached.response$; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment