day8 / re-frame-http-fx

A re-frame "effects handler" for performing Ajax tasks (via cljs-ajax)
MIT License
259 stars 27 forks source link

Make it possible to abort requests #3

Open deepxg opened 8 years ago

deepxg commented 8 years ago

It's common to want to abort an in-progress HTTP request, perhaps if a more recent request comes along (for example the case of multiple autocomplete requests), or if the user navigates away and it is no longer relevant.

How might I go about holding on to handlers (perhaps in :db) so that I can later handle them? Or would it be better to have an API that allowed a programmer to specify some sort of identifier for each request, such that multiple requests with the same identifier caused the previous one to abort?

danielcompton commented 8 years ago

This is an interesting question. There is the capability in cljs-ajax and goog.XhrIO to abort requests, the question is how to do this with re-frame. One possibility is that users specify an additional :id key, and we create a new fx called :abort-http-xhrio which takes ids to cancel.

Or would it be better to have an API that allowed a programmer to specify some sort of identifier for each request, such that multiple requests with the same identifier caused the previous one to abort?

This is an interesting idea which could simplify some kinds of code, like debouncing search results.

deepxg commented 8 years ago

Yeah, purely for debouncing I often have code like this:

(reg-event-fx
 :app/store-debounced-event
 (fn [{db :db} [_ event timestamp]]
   {:db (assoc-in db [:app/debounce (first event)] {:timestamp timestamp :event event})}))

(reg-event-fx
 :app/dispatch-debounced-event
 (fn [{db :db} [_ event timestamp]]
   (let [{latest-timestamp :timestamp event :event} (get-in db [:app/debounce (first event)])]
     (if (= timestamp latest-timestamp)
       {:db (update db :app/debounce dissoc (first event))
        :dispatch event}))))

(reg-event-fx
 :app/debounce-event
 (fn [{db :db} [_ event]]
   (let [timestamp (.getTime (js/Date.))]
     {:dispatch [:app/store-debounced-event event timestamp]
      :dispatch-later [{:ms 500 :dispatch [:app/dispatch-debounced-event event timestamp]}]})))

Which is easy to wrap in an effect handler. But I suspect there's a more general case, as I'll often want to kill off slow requests when navigating. There's even perhaps a wider (re-frame) point that currently effect handlers aren't that extensible, because they expose no data themselves - in this case it would be good to get a map out of all the current requests, so you could handle aborting requests without having to inspect or copy the actual implementation of http-fx.

flexsurfer commented 7 years ago

i need this functionality too, and for me would be better to have something like :on-request [:my-event], where request object will be passed, easiest and flexible way i think

flexsurfer commented 7 years ago

@danielcompton what do you think? can i make a PR?

kennethkalmer commented 7 years ago

I just came here looking for the same reason. I have some very long running queries, and currently I manually build the requests for those using the re-frame-http-fx plumbing (just so it looks the same). I'm refactoring and wanted things to be consistent and hit the two spots where I use (ajax/abort req) and just realized I'm falling short.

My gut says @flexsurfer's :on-request is a starting point that is definitely good enough.

The dispatcher can then decide what to do with the raw request, save it in the app-db and then manipulate it later. One caveat is that some cleanup needs to happen in both :on-success and :on-failure, but that is up to the caller and should be documented clearly.

nenadalm commented 5 years ago

Hi. I was thinking about it and maybe two additional keys could be added to the event?

:id - id under which request would register possibly inside some local atom when started and unregister when completed. If second request with the same id would come along before first one would finish, first one would be aborted. :when - predicate taking db and returning bool (e.g. (fn [db] (= (:page db) :user-list)). This predicate would say if request is still valid (user is still on the page, search query didn't change...) and if it returns failse, events shouldn't be triggered even if response arrived.

Ideally when event is in progress and :when condition changes, request should be immediately aborted. Only way to do this now is probably by attaching watch on the app-db atom?

I could send pr if this is acceptable.