Skip to content

Instantly share code, notes, and snippets.

@erikunha
Created July 10, 2023 13:23

Revisions

  1. Erik Cunha created this gist Jul 10, 2023.
    108 changes: 108 additions & 0 deletions cache.interceptor.spec.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,108 @@
    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();
    });
    });
    });
    65 changes: 65 additions & 0 deletions cache.interceptor.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,65 @@
    /* 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$;
    }
    }