0
votes

I'm working on an enterprise application that is being consumed as an angular element by another angular application.

Our application receives some input from the parent application which kicks off a bunch of api calls which then display data. One issue we are starting to run into is when we get quick, repeated inputs from the parent application.

For example, the parent application might input {"name": "Fred", "id": "abc"}, our application then starts to kick off a bunch of API calls using angular services which we subscribe to in our components. However, we are sometimes running into trouble if the parent component immediately were to input {"name": "Bob", "id": "xyz"} very shortly after the {"name": "Fred", "id": "abc"} input, before some of our API calls have completed. In these instances, we will sometimes be displaying data that belongs to the wrong person.

I can elaborate more if it would be helpful or post some modified code, don't think I'm allowed to share our actual code. I'm hoping to get some general advice or tips on how to deal with this type of situation.

One thing I have started to try is doing is checking to see if the subscription already exists each time we receive an input, resetting all of our display data and unsubscribing to the service, and then proceeding to start the subscription again. I'm not sure at this point if this has fixed the issue.

One other thing I was hoping to get some advice on is how to test for this situation. Is this something that could be tested using jasmine/karma (and somehow intentionally manipulating the response time in a mocked service), or is this something that would be more suited for Protractor/Cypress tests?

Thank you!

Edit -- Here is an imperfect stackblitz example, where the home component would be the focus, which accepts input from the app component

https://stackblitz.com/edit/angular-q4bv9y?file=src%2Fapp%2Fapp.module.ts

1
It would be great if you can create a stackblitz to show a trimmed down version of your code so we can see. As it sits now, it sounds like you need to do some debouncing or queuing to prevent the API from getting abused, but without any code it's impossible to say. - Phix
please share a code sample (or ideally stackblitz) that shows how you are constructing your data streams and passing them to your view. Generally speaking, for the scenario where you want to discard old API calls, because you received newer data and need to make new calls, the rxjs switchMap operator can help a lot. - BizzyBob
The problem that you are describing is a problem that RXJS is well suited to solve, but it requires some RXJS know how. I believe one step in the right direction would be to put the incoming users in a RXJS stream and then use the switchMap operator and then do your API calls. SwitchMap will then make sure that all API calls that is underway is cancelled when a new user comes in. - SnorreDan

1 Answers

0
votes

Here would be my approach:

export class HomeComponent {
    private foodSource = new Subject<string>();

    hasNoFavoriteFood = false
    isLoading = false;
    foodString$: Observable<string>;

    @Input() 
    set customer(customer: Customer) {
    this._customer = customer;
    this.foodSource.next(
            customer.hasFavoriteFood && customer.ID || null
        );
  }

    ngOnInit () {
        this.foodString$ = this.foodSource.pipe(
            // You might also want to add `debounce(ms)`
            // if the `setter` is called very often in a small amount of time
            // debounce(300ms) // once 300ms passed without the parent sending anything, proceed to making the req

            // Assumed that if `hasFavoriteFood === false`, a falsy value will be sent
            tap(id => this.hasNoFavoriteFood = !id),

            // Only the truthy values
            filter(v => !!v)
            switchMap(
                id => (
                    this.isLoading = true,
                    this.customerAPI.getCustomerFoodPreferences(id).pipe(
                        // Adding your retry logic
                        retryWhen(errors$ => errors$.pipe(/* ... */)),
                        finalize(() => this.isLoading = false)
                    )
                )
            ),

            // Might want to start with a default/falsy value
            startWith(null),
        )
    }
}

switchMap pretty much solves your problem. Suppose the parent sends A, switchMap will create an inner observable out of getCustomerFoodPreferences. But, when B is sent, if switchMap already has an active inner observable, it will be unsubscribed and a new one will be created, based on B.

finalize() is called whenever the source(in this case the observable produced by getCustomerFoodPreferences) is unsubscribed. That is, either when the source emits an error/complete notification, or when the source is explicitly unsubscribed, which is what switchMap does when a new value arrived.

And your template would consume it like this:

{{ foodString$ | async }}