1

Background

I feel like this is a very basic question, but I keep wondering if I'm missing something basic. Here is the scenario.

  1. On page load, I do an http request to load the initial model.
  2. I also have a websockets server sending push updates to the same model
  3. I might have other places where I update this model not via web sockets / http
  4. 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:

  1. Each client (e.g. a component) subscribes just to one observable

  2. 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).

  3. 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

  1. 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.

  2. What about my side question above? :) what is a good way to pass an Observable<Model> to a Subject.next? (other than observable.subscribe(model => subject.next(model));)

Related questions

How to handle/queue multiple same http requests with RxJs?

Eran Medan
  • 44,555
  • 61
  • 184
  • 276
  • 2
    if i read your question correctly, what you want sounds like some kind of an app state management. redux style `ngrx-store` is the one i can recommend. you would have a centralized state, updatable from different sources where you could have update actions as granular as you want (for example, different update rules if data comes from http request, websockets, this or that component) and then all consumers use single observable for geting model/state values. – dee zg Oct 28 '17 at 05:29

2 Answers2

1

You are on the right track.

The steps you would take to accomplish this are:

  1. In your service create an obseravable subject that stores your model. 2, Create a function that gets the model from the HTTP request.
    • In that function first check if the model is empty, if not return the observable you already have.
    • If the model is empty, get it from your HTTP requet.
  2. Have every component that needs the model, subscribe to the observable from #1
Ben Richards
  • 3,437
  • 1
  • 14
  • 18
1

I don't have a pure Rx solution, but I'd like to share some ideas gathered from tackling this problem in various ways.

Redux (Command-Query Separation)
I agree with @dee zg, the command-query separation principle makes reasoning about data flow much easier. Trying to find a pure Rx solution seems akin to trying to service your car with just one spanner. I first tackled this problem with a 'caching' service, but the result was complicated and brittle. Moving to Redux, the solution feels much better.

State handling
The singleton-like pattern that Ben Richards explains is key to avoiding multiple requests, but it's not just a case model empty / model full. The problem is HTTP requests take time and if a second client subscribes while the first is loading you get an overlap.
Is it likely, is it a problem? Depends on the application, but may be difficult to debug if it does occur.

Sequenced requests
The other aspect is that there may be a sequence of resources needed, and the sequence may be different for each component, as per the 'queuing of multiple requests' question you cited.
I think the sequence should be owned by and defined in the component. Take a payroll application which displays summary page that anyone can view, and detail pages that require a login token to access. Both use the data fetch service, but only the detail page needs to fetch the token - so token fetching can't be tied in to the data service.

My current structure

  • Components call a sequence of 'commands' on services upon init. The command returns just a promise to signal completion, to allow components to sequence and await the resources.

    initialize () {
      configService.checkConfig()
        .then(_ => {
          dataService.checkFiles(this.page)
        })
    }
    
  • Services define the command, but don't action it - they hand it off to a store action along with methods for handling fetch, fail, and timeout.

    checkConfig () {
      return store.dispatch('waitForFetch', {
        resource: 'config',
        fetch: this.getConfig   // this method defines the request
      })
    }
    
  • The Store action handles state for each resource, similar to your modelMap object, but stored in a Redux state branch so it can be easily reviewed in devtools.

    const actions = {
      waitForFetch: (context, {resource, fetch, fail, timeout}) => {
        return 
          isLoaded(resource)  ? Promise.resolve()
        : isInitial(resource) ? doFetch(resource, fetch, fail)
        : isLoading(resource) ? doWaitForLoading(resource, timeout)
        : null
      }
    }
    

It's not perfect, and it's not Rx (except for subscriptions to the store), but it's flexible, understandable, testable.

Richard Matsen
  • 20,671
  • 3
  • 43
  • 77