Saltar al contenido

Angular / RxJS 6: ¿Cómo evitar solicitudes HTTP duplicadas?

Solución:

Después de probar algunos métodos diferentes, encontré este que resuelve mi problema y solo hace una solicitud HTTP sin importar cuántos suscriptores:

class SharedService 
  someDataObservable: Observable;

  constructor(private http: HttpClient) 

  getSomeData(): Observable 
    if (this.someDataObservable) 
      return this.someDataObservable;
     else 
      this.someDataObservable = this.http.get('some/endpoint').pipe(share());
      return this.someDataObservable;
    
  

¡Todavía estoy abierto a sugerencias más eficientes!

Para los curiosos: compartir ()

Basado en su escenario simplificado, he creado un ejemplo funcional, pero la parte interesante es comprender lo que está sucediendo.

En primer lugar, he creado un servicio para simular http y evitar hacer llamadas HTTP reales:

export interface SomeData 
  some: 
    data: boolean;
  


@Injectable()
export class HttpClientMockService 
  private cpt = 1;

  constructor()  

  get(url: string): Observable 
    return of(
      some: 
        data: true
      
    )
    .pipe(
      tap(() =>
        console.log(`Request n°$this.cpt++ - URL "$url"`)
      ),
      // simulate a network delay
      delay(500)
    ) as any;
  

Dentro AppModule Reemplacé el HttpClient real para usar el simulado:

 provide: HttpClient, useClass: HttpClientMockService 

Ahora, el servicio compartido:

@Injectable()
export class SharedService 
  private cpt = 1;

  public myDataRes$: Observable = this
    .http
    .get('some-url')
    .pipe(share());

  constructor(private http: HttpClient)  

  getSomeData(): Observable 
    console.log(`Calling the service for the $this.cpt++ time`);
    return this.myDataRes$;
  

Si desde el getSomeData método devuelve una nueva instancia, tendrá 2 observables diferentes. Ya sea que use compartir o no. Entonces, la idea aquí es “preparar” la solicitud. CF myDataRes$. Es solo la solicitud, seguida de una share. Pero solo se declara una vez y devuelve esa referencia del getSomeData método.

Y ahora, si se suscribe desde 2 componentes diferentes al observable (resultado de la llamada de servicio), tendrá lo siguiente en su consola:

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time

Como puede ver, tenemos 2 llamadas al servicio, pero solo se realizó una solicitud.

¡Sí!

Y si desea asegurarse de que todo funcione como se esperaba, simplemente comente la línea con .pipe(share()):

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"

Pero … está lejos de ser ideal.

los delay en el servicio simulado es genial simular la latencia de la red. Pero también esconde un error potencial.

Desde la reproducción de stackblitz, vaya al componente second y descomente el setTimeout. Llamará al servicio después de 1 s.

Notamos que ahora, incluso si estamos usando share del servicio, tenemos lo siguiente:

Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"

¿Por qué eso? Porque cuando el primer componente se suscribe al observable, no sucede nada durante 500 ms debido al retraso (o la latencia de la red). Entonces, la suscripción aún está viva durante ese tiempo. Una vez que se realiza el retraso de 500 ms, el observable se completa (no es un observable de larga duración, al igual que una solicitud HTTP devuelve solo un valor, este también porque estamos usando of).

Pero share no es más que un publish y refCount. Publish nos permite multidifundir el resultado y refCount nos permite cerrar la suscripción cuando nadie está escuchando lo observable.

Entonces, con su solución usando compartir, si uno de sus componentes se crea más tarde de lo necesario para realizar la primera solicitud, aún tendrá otra solicitud.

Para evitar eso, no puedo pensar en ninguna solución brillante. Usando multidifusión tendríamos que usar el método de conexión, pero ¿dónde exactamente? ¿Hacer una condición y un contador para saber si es la primera llamada o no? No se siente bien.

Así que probablemente no sea la mejor idea y me alegraría si alguien pudiera proporcionar una solución mejor allí, pero mientras tanto, esto es lo que podemos hacer para mantener lo observable “vivo”:

  private infiniteStream$: Observable = new Subject().asObservable();

  public myDataRes$: Observable = merge(
    this
      .http
      .get('some-url'),
    this.infiniteStream$
  ).pipe(shareReplay(1))

Como el infiniteStream $ nunca se cierra, y estamos fusionando ambos resultados y usando shareReplay(1), ahora tenemos el resultado esperado:

Una llamada HTTP incluso si se realizan varias llamadas al servicio. No importa cuánto tiempo lleve la primera solicitud.

Aquí hay una demostración de Stackblitz para ilustrar todo eso: https://stackblitz.com/edit/angular-n9tvx7

A pesar de que las soluciones propuestas por otros antes del trabajo, me resulta molesto tener que crear manualmente campos en cada clase para cada diferente get/post/put/delete solicitud.

Mi solución se basa básicamente en dos ideas: una HttpService que gestiona todas las solicitudes http y un PendingService que gestiona qué solicitudes pasan realmente.

La idea es interceptar, no la solicitud en sí (podría haber usado un HttpInterceptor para eso, pero sería demasiado tarde porque las diferentes instancias de las solicitudes ya se habrían creado) pero la intención de realizar una solicitud, antes de que se realice.

Básicamente, todas las solicitudes pasan por este PendingService, que tiene un Set de solicitudes pendientes. Si una solicitud (identificada por su URL) no está en ese conjunto, significa que esta solicitud es nueva y tenemos que llamar al HttpClient (a través de una devolución de llamada) y guárdelo como una solicitud pendiente en nuestro conjunto, con su URL como keyy la solicitud observable como el valor.

Si luego se hace una solicitud a la misma URL, volvemos a verificar en el conjunto usando su URL, y si es parte de nuestro conjunto pendiente, significa … que está pendiente, por lo que devolvemos simplemente el observable que guardamos antes.

Siempre que finaliza una solicitud pendiente, llamamos a un método para eliminarla del conjunto.

Aquí hay un ejemplo asumiendo que estamos solicitando … No sé, ¿chihuahas?

Este sería nuestro pequeño ChihuahasService:

import  Injectable  from '@angular/core';
import  Observable  from 'rxjs';
import  HttpService  from '_services/http.service';

@Injectable(
    providedIn: 'root'
)
export class ChihuahuasService 

    private chihuahuas: Chihuahua[];

    constructor(private httpService: HttpService) 
    

    public getChihuahuas(): Observable 
        return this.httpService.get('https://api.dogs.com/chihuahuas');
    

    public postChihuahua(chihuahua: Chihuahua): Observable 
        return this.httpService.post('https://api.dogs.com/chihuahuas', chihuahua);
    


Algo como esto sería el HttpService:

import  HttpClient  from '@angular/common/http';
import  Observable  from 'rxjs';
import  share  from 'rxjs/internal/operators';
import  PendingService  from 'pending.service';

@Injectable(
    providedIn: 'root'
)
export class HttpService 

    constructor(private pendingService: PendingService,
                private http: HttpClient) 
    

    public get(url: string, options): Observable 
        return this.pendingService.intercept(url, this.http.get(url, options).pipe(share()));
    

    public post(url: string, body: any, options): Observable 
        return this.pendingService.intercept(url, this.http.post(url, body, options)).pipe(share());
    

    public put(url: string, body: any, options): Observable 
        return this.pendingService.intercept(url, this.http.put(url, body, options)).pipe(share());
    

    public delete(url: string, options): Observable 
        return this.pendingService.intercept(url, this.http.delete(url, options)).pipe(share());
    


Y finalmente, el PendingService

import  Injectable  from '@angular/core';
import  Observable  from 'rxjs';
import  tap  from 'rxjs/internal/operators';

@Injectable()
export class PendingService 

    private pending = new Map>();

    public intercept(url: string, request): Observable 
        const pendingRequestObservable = this.pending.get(url);
        return pendingRequestObservable ? pendingRequestObservable : this.sendRequest(url, request);
    

    public sendRequest(url, request): Observable 
        this.pending.set(url, request);
        return request.pipe(tap(() => 
            this.pending.delete(url);
        ));
    



De esta manera, incluso si 6 componentes diferentes están llamando al ChihuahasService.getChihuahuas(), solo se haría una solicitud y nuestra API para perros no se quejará.

Estoy seguro de que se puede mejorar (y agradezco los comentarios constructivos). Espero que alguien encuentre esto útil.

Al final de la artículo puedes encontrar las interpretaciones de otros usuarios, tú asimismo tienes la opción de dejar el tuyo si lo deseas.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *