0

So, I'm trying to wrap my head around rxjs/observable, in my test-case study I'm trying to build a simple angular(5) based web-app that should display some stats about geo entities, namely country, state, city. So I've crated changeView method that accepts three parameters(two optional) with respective ids.
At the beginning I wanted the app initially to check in my model if the country data is already loaded, if not then query the data, to save the retrieved data in the model, then to check if the stateId was specified and is included in country.states array then to check if already loaded, if not to query for the state and so on, until all the data was retrieved, or user has changed the input parameters, in that case all ongoing requests should stop(as far as I understood this part is done by switchMap operator), and new round of request should start. After couple of days I've understood that this is too complicated, so I've changed the plan and decided to store the data into the model only at the end. And it looks like it does what I intended to, but the sub-sequential requests are not being cancelled. Following is an excerpt from what I currently have:

of({countryId: 1, stateId: 1, cityId: 1}).pipe(
  debounceTime(500),
  switchMap(newState => {
    if ((newState !== null))
    {
     return this.geoStatsService.getCountry(newState.countryId);
    }
    else
    {
     return empty();
    }
  }, (outerValue, innerValue) => {
    if ("stateId" in outerValue && outerValue.stateId !== undefined)
    {
     // ToDo: Check that state is located in this county
     return forkJoin(of(innerValue), this.geoStatsService.getStates(outerValue.stateId));
    }
    else
    {
     return of(innerValue);
    }
  }),
  mergeAll()
  ).subscribe(res => {
     console.debug("---Subscription---");
     console.debug(res);
  });

I've seen that once the second request was fired, it won't be cancelled and the data will get to the subscription... It looks to me like I'm over-complicating it, and it could be done in more elegant way, which also will work as I intended, is it so? Additional question: Can I, or more precise question - should I extract the data from within the flow, or only in subscription method?

Jyrkka
  • 526
  • 1
  • 8
  • 26

2 Answers2

1

Hmmm... Given that Country/State/City might all need loading it seems to me that what you need is 3 nested switchMaps as follows:

interface Query { countryId: number; stateId?: number; cityId?: number }

// keep a cache to avoid reloading
const countryCache = new Map<number, Country>();
const stateCache = new Map<number, State>();
const cityCache = new Map<number, City>();

const query$: Observable<Query> = of({countryId: 1, stateId: 1, cityId: 1});

query$.pipe(
  debounceTime(500),
  switchMap((q: Query) => {
    if (!q || q.countryId === undefined) {
      return of([]);
    }

    // get country from cache or load from backend
    const country$ = countryCache.has(q.countryId) ?
      of(countryCache.get(q.countryId)) :
      this.geoStatsService.getCountry(q.countryId).do(country => {
        countryCache.set(q.countryId, country);
      });

    return country$.pipe(
      switchMap(country => {
        if (!country || !country.states.includes(q.stateId)) {
          return of([country]);
        }

        // get state from cache or load from backend
        const state$ = stateCache.has(q.stateId) ?
          of(stateCache.get(q.stateId)) :
          this.geoStatsService.getState(q.stateId).do(state => {
            stateCache.set(q.stateId, state);
          });

        return state$.pipe(
          switchMap(state => {
            if (!state || !state.cities.includes(q.cityId)) {
              return of([country, state]);
            }

            // get city from cache or load from backend
            const city$ = cityCache.has(q.cityId) ?
              of(cityCache.get(q.cityId)) :
              this.geoStatsService.getCity(q.cityId).do(city => {
                cityCache.set(q.cityId, city);
              });

            return city$.map(city => [country, state, city]);
          })
        );
      })
    );
  })
).subscribe(([country, state, city]) => {
  // ...
});
Miller
  • 2,742
  • 22
  • 20
  • Wow, so it tends to become over-complicated in terms of nesting,.. thank you very much for the effort and all your work writing this long example! As for my last question, I see that you extracted the data from within the flow, is it considered to be a good practice? – Jyrkka Feb 15 '18 at 09:46
  • 1
    Yes in terms of the nesting - I think improvements to the geoStatsService api could help (perhaps support eager fetching of states when looking up a country, or somehow allow country/state/city to be looked up in a single api call). In terms of where to extract the data, I tend to prefer my streams to be Observable rather than Observable – Miller Feb 15 '18 at 12:12
  • Well, you don't always want to give the user such a big slice of data and allow them to load data eagerly, but definitely to make the lookup method on server side a bit more complicated and to return the data of relevant country and state along with that of requested city - is an option. But for my specific example and understanding of how to chain these requests - you response is truly invaluable. Thanks a lot! – Jyrkka Feb 16 '18 at 12:21
0

Try using “flatMap” instead of “switchMap”, switchMap has its own cancellation system, that’s why sometimes you won’t get the subscription.

https://www.learnrxjs.io/operators/transformation/switchmap.html

Btw, you have to subscribe otherwise the code will not run.

itay oded
  • 978
  • 13
  • 22
  • Thanks for your reply, but I think didn't explain myself well, that's what I want - to cancel previous requests, since the user has changed the input - I don't need to show that info anymore, and there is a call to `subscribe()` in the end, you probably didn't notice... – Jyrkka Feb 15 '18 at 09:57
  • well so that's exactly what switchMap does. and yes I missed understood your problem, I tought you were complaining about the cancellation. – itay oded Feb 19 '18 at 08:46
  • Yeah, I know what the function of `switchMap` is I didn't know how to chain them, anyway I've got the answer I needed, but thank you for your participation, and willing to help.ממש מעריך את זה =) – Jyrkka Feb 19 '18 at 11:21