import { inject, Injectable, Optional, Inject } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpInterceptorFn, HttpRequest, HttpResponse, } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import hash from 'object-hash'; import { CachingHeaders } from './caching-headers.enum'; import { CachingStore } from './caching-store.enum'; import { P3X_HTTP_CACHE_CONFIG } from './http-cache-config.token'; import { HttpCacheConfig } from './http-cache-config'; const hashOptions = { algorithm: 'md5', encoding: 'hex', } as const; const globalCache = new Map>(); const DEFAULT_CONFIG: HttpCacheConfig = { behavior: CachingHeaders.Cache, store: CachingStore.Global, }; function httpToKey(httpRequest: HttpRequest): string { const body = JSON.parse(JSON.stringify(httpRequest.body)); return ( httpRequest.method + '@' + httpRequest.urlWithParams + '@' + hash(httpRequest.params, hashOptions) + '@' + hash(body, hashOptions) ); } function handle( httpRequest: HttpRequest, next: (req: HttpRequest) => Observable>, config: HttpCacheConfig, perInstanceCache: Map>, ): Observable> { const forcedCache = httpRequest.headers.get(CachingHeaders.Cache) !== null; const forcedNoneCache = httpRequest.headers.get(CachingHeaders.NoCache) !== null; let headers = httpRequest.headers.delete(CachingHeaders.NoCache); headers = headers.delete(CachingHeaders.Cache); httpRequest = httpRequest.clone({ headers }); if (forcedCache && forcedNoneCache) { throw new Error('You cannot use cache and non-cache header at once!'); } if (forcedNoneCache || (config.behavior === CachingHeaders.NoCache && !forcedCache)) { return next(httpRequest); } if (forcedCache || (config.behavior === CachingHeaders.Cache && !forcedNoneCache)) { const store = config.store === CachingStore.Global ? globalCache : perInstanceCache; const key = httpToKey(httpRequest); const lastResponse = store.get(key); if (lastResponse) { return of(lastResponse.clone()); } return next(httpRequest).pipe( tap((stateEvent) => { if (stateEvent instanceof HttpResponse) { store.set(key, stateEvent.clone()); } }), ); } console.error(config); console.error(httpRequest.headers); throw new Error('There is a configuration in your setup'); } /* Functional-interceptor path: there is no "module instance" to own a PerModule cache, so PerModule falls back to this module-level map (one per root injector, effectively Global-equivalent). The class-based interceptor below still honours true PerModule via its instance field. */ const functionalPerInstanceCache = new Map>(); /** * Functional interceptor. Use with: * `provideHttpClient(withInterceptors([p3xHttpCacheInterceptor]))` * or the convenience `provideP3xHttpCacheInterceptor(config)` helper. */ export const p3xHttpCacheInterceptor: HttpInterceptorFn = (httpRequest, next) => { const config = inject(P3X_HTTP_CACHE_CONFIG, { optional: true }) ?? DEFAULT_CONFIG; return handle(httpRequest, (req) => next(req), config, functionalPerInstanceCache); }; /** * Class-based interceptor — legacy shim kept for consumers still registering * via `HTTP_INTERCEPTORS` / `P3XHttpCacheInterceptorModule.forRoot(...)`. * Prefer `p3xHttpCacheInterceptor` (functional) for new code. */ @Injectable() export class HttpCacheInterceptorInterceptor implements HttpInterceptor { private readonly cachedData = new Map>(); private readonly httpCacheConfig: HttpCacheConfig; constructor(@Inject(P3X_HTTP_CACHE_CONFIG) @Optional() httpCacheConfigToken: HttpCacheConfig | null) { this.httpCacheConfig = httpCacheConfigToken ?? DEFAULT_CONFIG; } intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { return handle(httpRequest, (req) => next.handle(req), this.httpCacheConfig, this.cachedData); } }