Estoy implementando el caché usando BehaviorSubject
y multicast
. La secuencia devuelta de la memoria caché debe comenzar con una solicitud HTTP. También debería poder forzar la actualización del caché activando manualmente el next
tema. El enfoque común con dos temas esbozado por Poul Kruijt es bien conocido y sugerido en todas partes. Mi idea es encontrar una manera de lograr lo siguiente utilizando solo un tema a lo largo del ciclo de vida de una transmisión.
Sería fácil de lograr con multidifusión como esta
const cache = new BehaviorSubject(null); const shared = queryThatCompletes.pipe(multicast(cache)) as any; // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); // triggers http request shared.connect(); setTimeout(() => { // will only emit COMPLETE from subject shared.subscribe((values) => console.log(values)); }, 2000); // force refresh the cache cache.next();
pero dado que se completa el flujo de consulta HTTP, la segunda suscripción no obtiene ningún valor, solo la notificación COMPLETE
del asunto. Este comportamiento se describe en detalle aquí .
La otra opción es pasar una función de fábrica en lugar de la instancia del sujeto como esta:
const cache = ()=> new BehaviorSubject(null); const shared = queryThatCompletes.pipe(multicast(cache)) as any;
Esto volverá a crear el asunto, que se suscribirá a queryThatCompletes
y volverá a activar la solicitud HTTP. Pero las desventajas son la necesidad de llamar a la connect
varias veces y las consultas redundantes.
const cache = () => new BehaviorSubject(null); const shared = queryThatCompletes.pipe(multicast(cache)) as any; // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); // triggers http request shared.connect(); setTimeout(() => { // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); // triggers http request shared.connect(); }, 2000);
Así que simplemente implementé el flujo HTTP que no se completa solo y lo uso así:
const queryOnceButDontComplete = new Observable((observer) => { fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(data => observer.next(data)); return () => {}; }); const cache = new BehaviorSubject(null); const shared = queryOnceButDontComplete.pipe(multicast(cache)) as any; // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); // triggers http request shared.connect(); setTimeout(() => { // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); }, 2000);
Esto funciona, pero me pregunto si hay una manera de lograr lo que quiero sin el uso de observables personalizados. ¿Algunas ideas?
Lo mejor sería usar un shareReplay(1)
:
const shared = queryThatCompletes.pipe(shareReplay(1)); // sets up subscription, waits for connect shared.subscribe((values) => console.log(values)); // triggers http request shared.connect(); setTimeout(() => { shared.subscribe((values) => console.log(values)); }, 2000);
No estoy del todo seguro de qué están haciendo la subscribe
y la connect
allí, pero si solo está devolviendo una llamada HttpClient
get
, entonces debe devolver el observable shared
, y quien se suscriba primero, activará la solicitud http. No hay necesidad de connect
. Cualquier suscripción posterior esperará a que finalice la solicitud o recibirá el último valor emitido del observable.
Según su comentario, envolvamos esto en un servicio (no probado):
@Injectable() SomeDataService { readonly refresh$ = new BehaviorSubject(undefined); readonly get$ = this.httpClient.get(/*url here*/); readonly shared$ = this.refresh$.pipe( switchMap(() => this.get$), shareReplay(1) ); constructor(private httpClient: HttpClient) {} getData(): Observable<unknown> { return this.shared$; } refreshData(): void { this.refresh$.next(); } }
¿Esto tiene sentido? Básicamente, comienza con un tema de actualización, que se asigna a la llamada de red real. En el primer getData()
, se activa la solicitud de red. Cualquier llamada a getData()
después de eso obtendrá el valor almacenado en caché. Llamar a refreshData
actualizará los datos de cualquier suscripción
Hice un enfoque funcional que creo que es mucho más simple, usa dos observables refresh $ y el observable deseado, espero que esta solución pueda darle algunas ideas.
const makeRestartableCahcedObservable = (ob:Observable<any>)=>{ const refresh$ = new BehaviorSubject(null) return [ refresh$.pipe( switchMap(()=>ob), shareReplay(1) ), ()=>refresh$.next(null) ] } const [ob, refreshFun] = makeRestartableCahcedObservable(of('string')) ob.subscribe(console.log) setTimeout(()=>{refreshFun()}, 3000)
esto básicamente puede actualizar sus datos con una API mucho más simple, incluso puede envolverlos con un objeto para convertirlos en un proxy con función de actualización incluida
const makeRestartableCahcedObservable = (ob:Observable<any>)=>{ const refresh$ = new BehaviorSubject(null) const wrappedOb$ = refresh$.pipe(switchMap(()=>ob),shareReplay(1)) return { subscribe:(...args)=>wrappedOb$.subscribe(...args), pipe:(...funs)=>wrappedOb$.pipe(...funs), refresh:()=>refresh$.next(null) } } const ob = makeRestartableCahcedObservable(of('string')) ob.subscribe(console.error) setTimeout(()=>{ob.refresh()}, 3000)