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.