Background
I feel like this is a very basic question, but I keep wondering if I'm missing something basic. Here is the scenario.
- On page load, I do an http request to load the initial model.
- I also have a websockets server sending push updates to the same model
- I might have other places where I update this model not via web sockets / http
- More than one component might need the same model type at the same time, but I want only one http request to go out. In other words, all subscribers want "someone" to just do just one an http request and then update all of them, but no client will volunteer to "bell the cat..." (https://en.wikipedia.org/wiki/Belling_the_cat)
Here is what I want to achieve:
Each client (e.g. a component) subscribes just to one observable
If no one so far has requested that model (e.g. the observable is empty) something trigger that initial http request automatically. e.g. after all components got rendered / initialized, the DOM is presented to the user, if the observable is empty, initiate the http request automatically. (note that I need to differentiate between an empty observable and an http call that returned a null object). This should repeat on any route navigation or "page load" (very vague term in Angular world but you know what I mean).
If a websockets request comes in with a new version of that model, update the same observable, so clients don't care if it came from the initial http request or websockets request
Naive implementation
If lazy loading is not an issue (e.g. if I'm ok the model will always be loaded exactly once per a conceptual "page load") then I think I can do something like this
//totally made RoutingLifeCycle up, but I assume Angular has something like it
@Injectable()
class ModelLoadingService implements RoutingLifeCycle {
/**
/* only keep the "last" copy of the model for future subscribers.
*/
private modelSubject = new ReplaySubject<Model>(1);
/**
/* the observable clients will subscribe to.
*/
model$ = this.modelSubject.asObservable()
/**
/* Right before every conceptual "page load", where (except perhaps
/* the app component) many components get either destroyed or
/* initialized in a short time.
/* Again, I totally made this "onBeforeRouteChanged" method up but
/* I assume Angular has something like this
*/
onBeforeRouteChanged() {
//problem is that if there are no subscribers for it in this "page"
// then I wasted an http request. this is not "lazy"
// Side question... IS THERE A BETTER WAY THAN THIS?
// I can't pass an Observable<Model> to the Subject.next method
// And I can't .startWith as the subject is kind of immutable,
// And I can't "replace" model$ as the subscribers subscribed
// To the old one, (is there a way to "move the subscribers" to
// A new observable?) I feel I'm taking crazy pills
getModel().subscribe(model => this.modelSubject.next(model));
}
/**
* loads a model via a regular http request
*/
getModel(): Observable<Model> {
return http.get(...);
}
/**
* receives a model from a websockets message and multicasts it
*/
onWebsocketsMessage(message:string) {
// omitted error handling etc...
this.modelSubject.next(JSON.parse(message) as Model);
}
}
Another approach
If I want to avoid caring about page reload triggers, I guess I can use something like shareReplay(1, 1000)
(also adding handling of a "get by ID" situation not handled in the above example)
getModelById(id:string):Observable<Model> {
let modelObservable = this.modelMap.get(id);
if(!modelObservable) {
model = http.get(...).shareReplay(1, 1000);
this.modelMap.set(id, modelObservable);
}
return modelObservable;
}
Then I merge
this with the websockets subject and expose only that as the sole observable clients can use for that item. Is this the right way to do this?
Questions
What is an idiomatic way to achieve a similar behavior with Rx? Am I in the right direction? or still long way to go climbing the cliff.
What about my side question above? :) what is a good way to pass an
Observable<Model>
to aSubject.next
? (other thanobservable.subscribe(model => subject.next(model));
)