EDIT 2: What worked even better was shareReplay(1), as stated in the update of @karser's answer.
EDIT 1: What ended up working best was this:
@Injectable()
export class MyGlobalService {
private resource$;
private resource$Connected;
constructor(private http: Http) {
this.resource$Connected = false;
this.resource$ = this.http
.get('/api/resource')
.map((res: Response) => res.json())
.publishReplay(1);
}
getResource(): Observable<any> {
if (!this.resource$Connected) {
this.resource$.connect();
this.resource$Connected = true;
}
return this.resource$;
}
}
It only makes the AJAX call once, and it doesn't call until some consumer requires the resource.
ORIGINAL QUESTION:
I'm trying to cache an angular HTTP call and multicast the most recent result to all current and future subscribers. The ajax results won't change over the application lifetime, so I don't want to make any extra calls for a resource I already have. Therefore, I want it to continue to stay "connected" even when all the subscribers unsubscribe. Is this possible?
What I initially tried was this:
// in a global service
getResource(): Observable<any> {
return this.http
.get('/api/resource')
.map((res: Response) => res.json())
.publishLast()
.refCount();
}
This works well for multiple async pipes in the same component, but if that component is destroyed (and thus refCount goes to 0) the HTTP request will be repeated on a later instantiation of the component.
To combat this, I started manually caching the results:
resourceResults: any;
getResource(): Observable<any> {
if (resourceResults) {
return Observable.of(this.resourceResults);
}
return this.http
.get('/api/resource')
.map((res: Response) => res.json())
.do(x => this.resourceResults = x)
.publishLast()
.refCount();
}
This works fine, but I feel like there is a more rx way to do it.
I've tried using connect(), but that seems to suffer the same issue as my first example. Once all the subscribers have unsubscribed, using connect() causes the HTTP request to happen again
resource$ = this.http
.get('/api/resource')
.map((res: Response) => res.json())
.publishLast()
.refCount();
getResource(): Observable<any> {
this.resource$.connect();
return this.resource$;
}
Any ideas?
publishReplay/connect works. Here is the working plunker:
import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from "rxjs/Observable";
@Injectable()
export class YourService {
resource:Observable<any>;
constructor(private http: Http) {
this.resource = this.http.get('https://api.github.com/users/karser')
.map((res: Response) => res.json())
.do(res => console.log('response', res))
.publishReplay(1);
this.resource.connect();
}
}
The output:
Subscribing
response Object {login: "karser", id: 1675033…}
Unscibscribed
Subscribing once again
UPDATE: RxJS 5.4 has shareReplay operator which apparently does the same thing. See the updated plunkr
this.http.get('https://api.github.com/users/karser')
.map((res: Response) => res.json())
.shareReplay(1);
From pull request:
shareReplayreturns an observable that is the source multicasted over aReplaySubject. That replay subject is recycled on error from the source, but not on completion of the source. This makesshareReplayideal for handling things like caching AJAX results, as it's retryable. It's repeat behavior, however, differs from share in that it will not repeat the source observable, rather it will repeat the source observable's values.
First of all if you want to share this response during the entire application lifetime you should put it into a service and make sure you all your components are sharing the same instance.
You could theoretically make the source Observable to never complete. To be more precise you could make just the chain to never propage the complete signal but this is not recommended and potentially lead to unexpected behavior if you tried to use this Observable with operators such forkJoin() or toArray() and any similar that require the source Observable to properly complete.
Instead you can use publishReplay(1) to keep the last value emitted by the source Observable, refCount() to hold a single subscription to the source Observable while the cache is being queried and then take(1) to accept only one value and complete.
const cache = this.http.get('/api/resource')
.map((res: Response) => res.json())
.publishReplay(1)
.refCount()
.take(1);
So you can subscribe multiple observers at once and only one HTTP request will be performed. Then any subsequent subscriptions will hit the .publishReplay(1) that already has a cached value which is immediately propagated and take(1) completes the chain right away. So there'll be no subscription to this.http.get('/api/resource') and therefore no request will be made.
Do not use refCount. Create connectable observable manually, do connect, subscribe on it and ReplaySubject of the connectable will keep the last published value. Something like that:
let obs$ = Rx.Observable.interval(5000)
.multicast(new Rx.ReplaySubject(1));
obs$.connect();
// unsuscribe when service is destroyed???
let mainSubecription = obs$.subscribe(x=>console.log(x));
// create subscription (like async pipe)
let subscription;
setTimeout(()=> {
subscription = obs$.subscribe(x=>console.log('second', x));
}, 15000);
// remove subscription (like async pipe)
setTimeout(()=> {
subscription.unsubscribe();
}, 40000);
// the main subscription still gets data