Skip to content

Instantly share code, notes, and snippets.

@CodeByKwakes
Last active January 26, 2023 03:02
Show Gist options
  • Save CodeByKwakes/8f7282500c8c6612fa177ae87496d6cd to your computer and use it in GitHub Desktop.
Save CodeByKwakes/8f7282500c8c6612fa177ae87496d6cd to your computer and use it in GitHub Desktop.
Caching Http Interceptor #angular
import { NgModule } from '@angular/core';
import { httpInterceptorProviders } from './http-interceptors';
import { Cache } from './services/cache';
import { CacheMapService } from './services/cache-map.service';
@NgModule({
providers: [
httpInterceptorProviders,
CacheMapService,
{ provide: Cache, useClass: CacheMapService }
]
})
export class AppModule {}
import { HttpResponse } from '@angular/common/http';
export interface CacheEntry {
url: string;
response: HttpResponse<any>;
entryTime: number;
}
export const MAX_CACHE_AGE = 20000; // in milliseconds
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse } from '@angular/common/http';
import { Cache } from './cache';
import { CacheEntry, MAX_CACHE_AGE } from './cache-entry';
@Injectable()
export class CacheMapService implements Cache {
cacheMap = new Map<string, CacheEntry>();
get(req: HttpRequest<any>): HttpResponse<any> | null {
const entry = this.cacheMap.get(req.urlWithParams);
if (!entry) {
return null;
}
const isExpired = Date.now() - entry.entryTime > MAX_CACHE_AGE;
return isExpired ? null : entry.response;
}
put(req: HttpRequest<any>, res: HttpResponse<any>): void {
const entry: CacheEntry = {
url: req.urlWithParams,
response: res,
entryTime: Date.now()
};
this.cacheMap.set(req.urlWithParams, entry);
this.deleteExpiredCache();
}
private deleteExpiredCache() {
this.cacheMap.forEach((entry) => {
if (Date.now() - entry.entryTime > MAX_CACHE_AGE) {
this.cacheMap.delete(entry.url);
}
});
}
}
import { HttpRequest, HttpResponse } from '@angular/common/http';
export abstract class Cache {
abstract get(req: HttpRequest<any>): HttpResponse<any> | null;
abstract put(req: HttpRequest<any>, res: HttpResponse<any>): void;
}
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpResponse,
HttpHandler
} from '@angular/common/http';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheMapService } from '../services/cache-map.service';
const CACHABLE_URL = '/api/booksSearch';
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: CacheMapService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
if (!this.isRequestCachable(req)) {
return next.handle(req);
}
const cachedResponse = this.cache.get(req);
if (cachedResponse !== null) {
return of(cachedResponse);
}
return next.handle(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
this.cache.put(req, event);
}
})
);
}
private isRequestCachable(req: HttpRequest<any>) {
return req.method === 'GET' && req.url.indexOf(CACHABLE_URL) > -1;
}
}
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { LoggingInterceptor } from './logging-interceptor';
import { CachingInterceptor } from './caching-interceptor';
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
];
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpResponse
} from '@angular/common/http';
import { finalize, tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const startTime = Date.now();
let status: string;
return next.handle(req).pipe(
tap(
(event) => {
status = '';
if (event instanceof HttpResponse) {
status = 'succeeded';
}
},
(error) => (status = 'failed')
),
finalize(() => {
const { method, urlWithParams } = req;
const elapsedTime = Date.now() - startTime;
const message = `${method} ${urlWithParams} ${status} in ${elapsedTime}ms`;
this.logDetails(message);
})
);
}
private logDetails(msg: string) {
console.log(msg);
}
}
// cache.ts
export abstract class Cache {
  abstract get(req: HttpRequest<any>): HttpResponse<any> | null;
  abstract put(req: HttpRequest<any>, res: HttpResponse<any>): void;
}
// cache-entry.ts
export interface CacheEntry {
  url: string;
  response: HttpResponse<any>;
  entryTime: number;
}

export const MAX_CACHE_AGE = 20000; // in milliseconds
// cache-map.service.ts
@Injectable()
export class CacheMapService implements Cache {
  cacheMap = new Map<string, CacheEntry>();

  get(req: HttpRequest<any>): HttpResponse<any> | null {
    const entry = this.cacheMap.get(req.urlWithParams);

    if (!entry) {
      return null;
    }

    const isExpired = Date.now() - entry.entryTime > MAX_CACHE_AGE;
    
    return isExpired ? null : entry.response;
  }

  put(req: HttpRequest<any>, res: HttpResponse<any>): void {
    const entry: CacheEntry = {
      url: req.urlWithParams,
      response: res,
      entryTime: Date.now()
    };
    this.cacheMap.set(req.urlWithParams, entry);
    this.deleteExpiredCache();
  }

  private deleteExpiredCache() {
    this.cacheMap.forEach((entry) => {
      if (Date.now() - entry.entryTime > MAX_CACHE_AGE) {
        this.cacheMap.delete(entry.url);
      }
    });
  }
}
// caching-interceptor.ts
const CACHABLE_URL = '/api/booksSearch';

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: CacheMapService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRequestCachable(req)) {
      return next.handle(req);
    }

    const cachedResponse = this.cache.get(req);

    if (cachedResponse !== null) {
      return of(cachedResponse);
    }

    return next.handle(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          this.cache.put(req, event);
        }
      })
    );
  }

  private isRequestCachable(req: HttpRequest<any>) {
    return req.method === 'GET' && req.url.indexOf(CACHABLE_URL) > -1;
  }
}
// logging-interceptor.ts
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const startTime = Date.now();
    let status: string;

    return next.handle(req).pipe(
      tap(
        (event) => {
          status = '';
          if (event instanceof HttpResponse) {
            status = 'succeeded';
          }
        },
        (error) => (status = 'failed')
      ),
      finalize(() => {
        const elapsedTime = Date.now() - startTime;
        const message = `${req.method} ${req.urlWithParams} ${status} in ${elapsedTime}ms`;

        this.logDetails(message);
      })
    );
  }

  private logDetails(msg: string) {
    console.log(msg);
  }
}
// index.ts
export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
];
// app.module.ts
@NgModule({
  providers: [
    httpInterceptorProviders,
    CacheMapService,
    { provide: Cache, useClass: CacheMapService }
  ]
})
export class AppModule {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment