RSS Git Download  Clone
Raw Blame History 4kB 120 lines
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<string, HttpResponse<any>>();

const DEFAULT_CONFIG: HttpCacheConfig = {
    behavior: CachingHeaders.Cache,
    store: CachingStore.Global,
};

function httpToKey(httpRequest: HttpRequest<any>): 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<unknown>,
    next: (req: HttpRequest<unknown>) => Observable<HttpEvent<unknown>>,
    config: HttpCacheConfig,
    perInstanceCache: Map<string, HttpResponse<any>>,
): Observable<HttpEvent<unknown>> {
    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<string, HttpResponse<any>>();

/**
 * 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<string, HttpResponse<any>>();
    private readonly httpCacheConfig: HttpCacheConfig;

    constructor(@Inject(P3X_HTTP_CACHE_CONFIG) @Optional() httpCacheConfigToken: HttpCacheConfig | null) {
        this.httpCacheConfig = httpCacheConfigToken ?? DEFAULT_CONFIG;
    }

    intercept(httpRequest: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
        return handle(httpRequest, (req) => next.handle(req), this.httpCacheConfig, this.cachedData);
    }
}