Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancel observable based on payload rather than effect

I have a service that makes http requests to a backend I don't control to get marketing page contents. Sometimes, I need to load more than one piece of marketing content at the same time. I can create an effect that calls the service.

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .switchMap(({ payload }) => this.marketingService.getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
  )

This works fine, and I can call store.dispatch(Action.LoadMarketingContent('A')).

The problem is that if I need to load more than one piece of marketing content at a time the .switchMap will cancel the previous request.

store.dispatch(Action.LoadMarketingContent('A'));
store.dispatch(Action.LoadMarketingContent('B'));
// Only `'B'` is loaded since 'A' gets canceled before it completes

I can use .mergeMap instead of .switchMap, but then duplicate requests won't get canceled.

I can also use separate actions to load each piece of marketing content, but that would require creating an action and effects for each piece.

Is there a way that I can use .switchMap to cancel only requests for the same content (where payload is the same?) or another way to make simultaneous different requests while canceling duplicate requests in the same stream?

like image 652
Explosion Pills Avatar asked Feb 02 '18 03:02

Explosion Pills


1 Answers

If you introduce a CANCEL_MARKETING_CONTENT action you could do something like this with mergeMap:

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .mergeMap(({ payload }) => this.marketingService
    .getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
    .takeUntil(this.actions$.ofType(CANCEL_MARKETING_CONTENT))
  );

Basically, that would let you load as many pieces of marketing content as you like, but it would be up to you to cancel any pending loads by dispatching an CANCEL_MARKETING_CONTENT action before you dispatch the LOAD_MARKETING_CONTENT action(s).

For example, to load only piece A, you'd do this:

store.dispatch(Action.CancelMarketingContent());
store.dispatch(Action.LoadMarketingContent('A'));

And to load both pieces A and B, you'd do this:

store.dispatch(Action.CancelMarketingContent());
store.dispatch(Action.LoadMarketingContent('A'));
store.dispatch(Action.LoadMarketingContent('B'));

Actually, there is a similar - but neater - way of doing it and it doesn't involve using another action.

You can use the dispatch of the same action with the same payload as the cancellation trigger. For example:

@Effect()
marketingContent$ = this.actions$
  .ofType(LOAD_MARKETING_CONTENT)
  .mergeMap(({ payload }) => this.marketingService
    .getContent(payload)
    .map(content => Action.LoadMarketingContentComplete(content))
    .takeUntil(this.actions$
      .ofType(LOAD_MARKETING_CONTENT)
      .skip(1)
      .filter(({ payload: next }) => next === payload)
    )
  );

From memory, skip will be needed to skip the action currently being handled by the effect. And the answer assumes that payload is "A" or "B", etc.

like image 115
cartant Avatar answered Nov 06 '22 22:11

cartant