11

So I have use case where I update the api request when the map is moved - but it could generate several rapid fire requests with small map movements - and I want to cancel all the inflight requests except for the last one. I can use debounce to only send requests after a delay. However I still want to cancel any old requests if they happen to still be in process.

const fetchNearbyStoresEpic = action$ =>
  action$.ofType(FETCH_NEARBY_STORES)
    .debounceTime(500)
    .switchMap(action =>
      db.collection('stores')
        .where('location', '<=', action.payload.max).
        .where('location', '>=', action.payload.min)
        .map(response => fetchNearbyStoresFulfilled(response))
        .takeUntil(action$.ofType(FETCH_STORES_CANCELLED))
    );

I see that you can use takeUntil but you need to explicitly fire a cancel action. I see in the docs that switchMap will take the latest and cancel all the others - do I have to implement a cancel interface in my api call? In this case it would be a firebase query to firestore.

MonkeyBonkey
  • 40,521
  • 63
  • 217
  • 426
  • Isn't [`auditTime`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-auditTime) what you're looking for instead of `debounceTime`? – martin May 03 '18 at 17:53
  • I think debounceTime is better since we don't need to give results after 500ms if the user is still moving the map – MonkeyBonkey May 03 '18 at 18:29
  • To clarify, you're debouncing the map moves and, 500 ms after the last move, you initiate a request. Is it the case that you want to cancel that request immediately if the map again moves, rather than waiting for the debouce and cancellation via the `switchMap`? – cartant May 03 '18 at 21:07
  • @MonkeyBonkey, how do you know when come last value from stream? If stream doesn't complete, you can't get the last one. Because you don't know when last will come – Yerkon May 04 '18 at 05:53
  • @cartant well that would be a bonus so covering that scenario would be nice - but I'm also asking the simpler case of say the request takes 10 seconds, so: debounce send request 1, debounce and send request 2, but request 1 still has not completed. How does request 1 get cancelled, does switchmap somehow hold a reference to the original request 1? I suppose I don't really understand how switchmap works. How does it trigger the cancellation on the firebase query? – MonkeyBonkey May 04 '18 at 14:10

2 Answers2

18

From a comment I made in a GitHub issue:

Because they have a time dimension, there are multiple flattening strategies for observables:

  • With mergeMap (which has flatMap as an alias), received observables are subscribed to concurrently and their emitted values are flattened into the output stream.
  • With concatMap, received observables are queued and are subscribed to one after the other, as each completes. (concatMap is mergeMap with a concurrency of one.)
  • With switchMap, when an observable is received it's subscribed to and any subscription to a previously received observable is unsubscribed.
  • With exhaustMap, when an observable is received it's subscribed to unless there is a subscription to a previously received observable and that observable has not yet completed - in which case the received observable is ignored.

So, like Mark said in his answer, when switchMap receives a subsequent action, it will unsubscribe from any incomplete request.

However, the request won't be cancelled until the debounced action makes it to the switchMap. If you want to cancel any pending requests immediately upon another move - rather than wait for the debounce duration - you can use takeUntil with the FETCH_NEARBY_STORES action:

const fetchNearbyStoresEpic = action$ =>
  action$.ofType(FETCH_NEARBY_STORES)
    .debounceTime(500)
    .switchMap(action =>
      db.collection('stores')
        .where('location', '<=', action.payload.max).
        .where('location', '>=', action.payload.min)
        .map(response => fetchNearbyStoresFulfilled(response))
        .takeUntil(action$.ofType(FETCH_NEARBY_STORES))
    );

That should effect the immediate unsubscription from a request upon another move. (Off the top of my head, I cannot recall the behaviour of action$ in redux-observable. It's possible that you might need to append a skip(1) to the observable passed to takeUntil. Try it and see.)

And, as Mark mentioned, this is predicated on the underlying implementation cancelling the request upon unsubscription.

cartant
  • 50,834
  • 17
  • 138
  • 173
5

switchMap will abandon its previous observable when a new emission is send through it. Depending on your underlying HTTP library and if it supports cancellation (Observable aware) this should suffice.

Because no implementation details have been provided in your question you will have to look into fetchNearbyStoresFulfilled to see if it uses an Observable aware http client. If it internally is using promises then no cancellation support is provided.

Mark van Straten
  • 8,503
  • 2
  • 34
  • 56
  • `fetchNearbyStoresFulfilled` is just a redux action so it generates a simple action object, but is not a promise, db.collection('stores').where.. is the async promise from firebase in this case – MonkeyBonkey May 04 '18 at 14:13