From ada38257cc3532319de0a17e4c2c1546736b8b57 Mon Sep 17 00:00:00 2001 From: Sonia Zorba Date: Wed, 23 Oct 2024 12:28:40 +0200 Subject: [PATCH] Prevented duplicated parallel HTTP calls --- src/app/interceptors/cache.interceptor.ts | 32 ++++++++++++++--------- src/app/services/cache.service.ts | 9 ++++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/app/interceptors/cache.interceptor.ts b/src/app/interceptors/cache.interceptor.ts index b62ecba..9a9daa0 100644 --- a/src/app/interceptors/cache.interceptor.ts +++ b/src/app/interceptors/cache.interceptor.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpResponse } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { CacheService } from '../services/cache.service'; -import { tap } from 'rxjs/operators'; +import { filter, first, map, shareReplay } from 'rxjs/operators'; @Injectable() export class CacheInterceptor implements HttpInterceptor { constructor(private readonly cacheService: CacheService) {} + public readonly store: Record>> = {}; + intercept(request: HttpRequest, next: HttpHandler): Observable> { // Don't cache if it's not a GET request if (request.method !== 'GET') { @@ -26,20 +28,26 @@ export class CacheInterceptor implements HttpInterceptor { // Checked if there is cached data for this URI const cachedResponse = this.cacheService.getFromCache(request); if (cachedResponse) { - // In case of parallel requests to same URI, - // return the request already in progress - // otherwise return the last cached data - return cachedResponse instanceof Observable ? cachedResponse : of(cachedResponse.clone()); + return cachedResponse; } // If the request of going through for first time // then let the request proceed and cache the response - return next.handle(request).pipe( - tap(event => { - if (event instanceof HttpResponse) { - this.cacheService.addToCache(request, event); - } - }) + const response = next.handle(request).pipe( + filter(res => res instanceof HttpResponse), + // The default Observable behavior is creating a new stream for each subscription. + // With shareReplay we avoid the default behavior and instead we execute the stream only once, + // then the result of that stream will be replayed to each new subscriber. + shareReplay(1) + ); + this.cacheService.addToCache(request, response); + return response.pipe( + // Ensures that when the first value is received, the observable will automatically unsubscribe + // and stop listening for any further emissions. + first(), + // Clones the response, to avoid that further modifications to the data will affect the data + // within the cache. + map(event => (event as HttpResponse).clone()) ); } } diff --git a/src/app/services/cache.service.ts b/src/app/services/cache.service.ts index 7a02db9..d7a35c1 100644 --- a/src/app/services/cache.service.ts +++ b/src/app/services/cache.service.ts @@ -1,8 +1,9 @@ -import { HttpRequest, HttpResponse } from '@angular/common/http'; +import { HttpEvent, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; interface Cache { - response: HttpResponse; + response: Observable>; time: number; } @@ -12,14 +13,14 @@ const MAX_CACHE_AGE = 5 * 60 * 1000; // 5 minuti export class CacheService { cacheMap = new Map(); - getFromCache(req: HttpRequest): HttpResponse | undefined { + getFromCache(req: HttpRequest): Observable> | undefined { const cached = this.cacheMap.get(req.urlWithParams); if (!cached || Date.now() - cached.time > MAX_CACHE_AGE) return undefined; return cached.response; } - addToCache(req: HttpRequest, response: HttpResponse): void { + addToCache(req: HttpRequest, response: Observable>): void { this.cacheMap.set(req.url, { response, time: Date.now() }); } }