diff --git a/.gitignore b/.gitignore index c357d4b..0192eda 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pom.xml.asc .hgignore .hg/ .DS_Store +*.~undo-tree~ diff --git a/README.md b/README.md index c289371..7e48a71 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,12 @@ Interceptor and helpers to register and unregister (background-)tasks (FXs) in y * register / unregister tasks / fxs via one line interceptor injection * support multiple and any fx on-completion keys * subscriptions for tasks list and running task boolean - * running task boolean can be quick filtered by task id + * running task boolean can be quick filtered by task name * events to register / unregister tasks yourself * helpers to register / unregister tasks into db yourself +Alsoe works for async coeffects injections, see https://github.com/jtkDvlp/re-frame-async-coeffects. + ## Getting started ### Get it / add dependency @@ -24,6 +26,8 @@ Add the following dependency to your `project.clj`:<br> ### Usage +See api docs [![cljdoc badge](https://cljdoc.org/badge/jtk-dvlp/re-frame-tasks)](https://cljdoc.org/d/jtk-dvlp/re-frame-tasks/CURRENT) + ```clojure (ns jtk-dvlp.your-project (:require @@ -32,40 +36,37 @@ Add the following dependency to your `project.clj`:<br> (rf/reg-event-fx :some-event - ;; give it the fx to identify the task emitted by this event - [(tasks/as-task :some-fx) - ;; of course you can use this interceptor for more than one fx in a event call - ;; futher more you can give it the handler keys to hang in finishing the task - (tasks/as-task :some-other-fx :on-done :on-done-with-errors)] + ;; give it a name and fx keys to identify the task and effects emitted by this event. + [(tasks/as-task :some-task [:some-fx :some-other-fx]) + ;; futher more you can give it the handler keys to hang in finishing the tasks effects + ;; (tasks/as-task :some-task + ;; [[:some-fx :on-done :on-done-with-errors] + ;; [:some-other-fx :on-yuhu :on-oh-no]]) + ;; last but not least supports the special effect :fx giving an path for the fx to monitor. + ;; (tasks/as-task :some-task [[[:fx 1] :on-done ,,,] ; you need to give it the index of the effect within :fx vector to monitor. + ;; ,,,]) + ] (fn [_ _] {:some-fx - {,,, - ;; you can give the tasks an id (default: uuid), see subscription `:jtk-dvlp.re-frame.tasks/running?` for usage. - ::tasks/id :some-important-stuff - :label "Do some fx" + {:label "Do some fx" :on-success [:some-event-success] :on-error [:some-event-error] - ;; calling this by `:some-fx` will unregister the task via `tasks/as-task` - :on-completed [:some-event-completed]} + :on-done [:some-event-completed]} :some-other-fx {,,, :label "Do some other fx" - ;; calling this by some-fx will unregister the task via `tasks/as-task` ;; `:on-done-with-error` will also untergister the task when called by `:some-other-fx` :on-done [:some-other-event-completed]}})) (defn app-view [] (let [block-ui? - ;; for sugar you can give it also a pred (called with the task id) e.g. a set of task ids to filter the running tasks. - (rf/subscribe [:jtk-dvlp.re-frame.tasks/running?]) - - any-running-task? (rf/subscribe [:jtk-dvlp.re-frame.tasks/running?]) + ;; for sugar you can give it also a task name to filter the running tasks. some-important-stuff-running? - (rf/subscribe [:jtk-dvlp.re-frame.tasks/running? #{:some-important-stuff}]) + (rf/subscribe [:jtk-dvlp.re-frame.tasks/running? :some-important-stuff]) tasks (rf/subscribe [:jtk-dvlp.re-frame.tasks/tasks])] @@ -75,10 +76,10 @@ Add the following dependency to your `project.clj`:<br> [:div "some app content"] [:ul "task list" - ;; each task is the original fx map plus an `::tasks/id` and `::tasks/effect` - (for [{:keys [label :jtk-dvlp.re-frame.tasks/id] :as _task} @tasks] + ;; each task is a map with the original event vector plus name and id. + (for [{:keys [:jtk-dvlp.re-frame.tasks/id] :as task} @tasks] ^{:key id} - [:li label])] + [:li task])] (when @block-ui? [:div "this div blocks the UI if there are running tasks"])]))) diff --git a/dev.cljs.edn b/dev.cljs.edn index bce2d4c..ce3a9f2 100644 --- a/dev.cljs.edn +++ b/dev.cljs.edn @@ -10,4 +10,4 @@ ,,,} {:main - jtk-dvlp.user} + jtk-dvlp.your-project} diff --git a/dev/jtk_dvlp/user.cljs b/dev/jtk_dvlp/user.cljs deleted file mode 100644 index 9a9432c..0000000 --- a/dev/jtk_dvlp/user.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns jtk-dvlp.user) diff --git a/dev/jtk_dvlp/your_project.cljs b/dev/jtk_dvlp/your_project.cljs index 875d7e9..50780a0 100644 --- a/dev/jtk_dvlp/your_project.cljs +++ b/dev/jtk_dvlp/your_project.cljs @@ -1,57 +1,189 @@ -(ns jtk-dvlp.your-project +(ns ^:figwheel-hooks jtk-dvlp.your-project (:require + [cljs.pprint] + [cljs.core.async :refer [timeout]] + [jtk-dvlp.async :refer [go <!] :as a] + + [goog.dom :as gdom] + [reagent.dom :as rdom] + [re-frame.core :as rf] + [jtk-dvlp.re-frame.async-coeffects :as acoeffects] [jtk-dvlp.re-frame.tasks :as tasks])) +(defn- some-async-stuff + [] + (go + (<! (timeout 5000)) + :result)) + +(rf/reg-fx :some-fx + (fn [{:keys [on-complete] :as x}] + (println ":some-fx" x) + (go + (let [result (<! (some-async-stuff))] + (println ":some-fx finished") + (rf/dispatch (conj on-complete result)))))) + +(rf/reg-fx :some-other-fx + (fn [{:keys [bad-data on-done on-done-with-errors] :as x}] + (println ":some-other-fx" x) + (go + (try + (when bad-data + (throw (ex-info "bad data" {:code :bad-data}))) + (let [result (<! (some-async-stuff))] + (rf/dispatch (conj on-done result))) + (catch :default e + (println ":some-other-fx error" e) + (rf/dispatch (conj on-done-with-errors e))) + (finally + (println ":some-other-fx finished")))))) + +(acoeffects/reg-acofx :some-acofx + (fn [cofxs & [bad-data :as args]] + (println ":some-acofx") + (go + (when bad-data + (throw (ex-info "bad data" {:code :bad-data}))) + (let [result + (->> args + (apply some-async-stuff) + (<!) + (assoc cofxs :some-acofx))] + + (println ":some-acofx finished") + result)))) + (rf/reg-event-fx :some-event ;; give it the fx to identify the task emitted by this event - [(tasks/as-task :some-fx) - ;; of course you can use this interceptor for more than one fx in a event call - ;; futher more you can give it the handler keys to hang in finishing the task - (tasks/as-task :some-other-fx :on-done :on-done-with-errors)] + [(tasks/as-task :some-task + [:some-fx + [:some-other-fx :on-done :on-done-with-errors] + [[:fx 1] :on-done]]) + (acoeffects/inject-acofx :some-acofx)] + (fn [_ _] + (println "handler") + {:some-fx + {,,, + ;; you can give the tasks an id (default: uuid), see subscription `:jtk-dvlp.re-frame.tasks/running?` for usage. + :on-success [:some-event-success] + :on-error [:some-event-error] + ;; calling this by `:some-fx` will unregister the task via `tasks/as-task` + :on-complete [:some-event-completed]} + + :some-other-fx + {,,, + ;; calling this by some-fx will unregister the task via `tasks/as-task` + ;; `:on-done-with-error` will also untergister the task when called by `:some-other-fx` + :on-done [:some-other-event-completed]} + + :fx + [[:some-fx + {:on-success [:some-event-success] + :on-error [:some-event-error] + :on-complete [:some-event-completed]}] + + ;; same with :fx effect referenzed via path [:fx 1] + [:some-other-fx + {:on-done [:some-other-event-completed]}]]})) + +(rf/reg-event-fx :some-bad-event + ;; give it the fx to identify the task emitted by this event + [(tasks/as-task :some-task + [:some-fx + {:effect-key [:some-other-fx] + :completion-keys #{:on-done :on-done-with-errors}}]) + (acoeffects/inject-acofx [:some-acofx :bad-data])] (fn [_ _] + (println "handler") {:some-fx {,,, ;; you can give the tasks an id (default: uuid), see subscription `:jtk-dvlp.re-frame.tasks/running?` for usage. - ::tasks/id :some-important-stuff - :label "Do some fx" :on-success [:some-event-success] :on-error [:some-event-error] ;; calling this by `:some-fx` will unregister the task via `tasks/as-task` - :on-completed [:some-event-completed]} + :on-complete [:some-event-completed]} :some-other-fx {,,, - :label "Do some other fx" ;; calling this by some-fx will unregister the task via `tasks/as-task` ;; `:on-done-with-error` will also untergister the task when called by `:some-other-fx` :on-done [:some-other-event-completed]}})) +(rf/reg-event-fx :some-other-bad-event + ;; give it the fx to identify the task emitted by this event + [(tasks/as-task :some-task + [:some-fx + {:effect-key [:some-other-fx] + :completion-keys #{:on-done :on-done-with-errors}}]) + (acoeffects/inject-acofx :some-acofx)] + (fn [_ _] + (println "handler") + {:some-fx + {,,, + ;; you can give the tasks an id (default: uuid), see subscription `:jtk-dvlp.re-frame.tasks/running?` for usage. + :on-success [:some-event-success] + :on-error [:some-event-error] + ;; calling this by `:some-fx` will unregister the task via `tasks/as-task` + :on-complete [:some-event-completed]} + + :some-other-fx + {,,, + :bad-data true + ;; calling this by some-fx will unregister the task via `tasks/as-task` + ;; `:on-done-with-error` will also untergister the task when called by `:some-other-fx` + :on-done [:some-other-event-completed]}})) + +(rf/reg-event-db :some-event-completed + (fn [db _] + db)) + +(rf/reg-event-db :some-other-event-completed + (fn [db _] + db)) + (defn app-view [] (let [block-ui? - ;; for sugar you can give it also a pred (called with the task id) e.g. a set of task ids to filter the running tasks. - (rf/subscribe [:jtk-dvlp.re-frame.tasks/running?]) - - any-running-task? (rf/subscribe [:jtk-dvlp.re-frame.tasks/running?]) - some-important-stuff-running? - (rf/subscribe [:jtk-dvlp.re-frame.tasks/running? #{:some-important-stuff}]) - tasks (rf/subscribe [:jtk-dvlp.re-frame.tasks/tasks])] (fn [] [:<> - [:div "some app content"] + [:button {:on-click #(rf/dispatch [:some-event])} + "exec some event"] + [:button {:on-click #(rf/dispatch [:some-bad-event])} + "exec some bad event"] + [:button {:on-click #(rf/dispatch [:some-other-bad-event])} + "exec some other bad event"] - [:ul "task list" + [:ul "task list " (count @tasks) ;; each task is the original fx map plus an `::tasks/id` and `::tasks/effect` - (for [{:keys [label :jtk-dvlp.re-frame.tasks/id] :as _task} @tasks] + (for [{:keys [::tasks/id] :as task} (vals @tasks)] ^{:key id} - [:li label])] + [:li [:pre (with-out-str (cljs.pprint/pprint task))]])] (when @block-ui? [:div "this div blocks the UI if there are running tasks"])]))) + + +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; re-frame setup + +(defn- mount-app + [] + (rdom/render + [app-view] + (gdom/getElement "app"))) + +(defn ^:after-load on-reload + [] + (rf/clear-subscription-cache!) + (mount-app)) + +(defonce on-init + (mount-app)) diff --git a/dev/user.clj b/dev/user.clj index ffe0c31..a9a49f0 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -4,4 +4,8 @@ (defn fig-init [] - (figwheel.main.api/start "dev")) + (figwheel/start {:mode :serve} "dev")) + +(defn cljs-repl + [] + (figwheel/cljs-repl "dev")) diff --git a/project.clj b/project.clj index 0e5b4f3..3fc4e97 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject jtk-dvlp/re-frame-tasks "1.0.1" +(defproject jtk-dvlp/re-frame-tasks "2.0.0-SNAPSHOT" :description "A re-frame interceptor and helpers to register / unregister (background-)tasks" @@ -22,17 +22,18 @@ ^{:protect false} [:target-path] - :profiles - {:provided - {:dependencies - [[org.clojure/clojure "1.10.0"] - [org.clojure/clojurescript "1.10.773"] - - [re-frame "0.12.0"]]} + :dependencies + [[org.clojure/clojure "1.10.0"] + [org.clojure/clojurescript "1.10.773"] + [jtk-dvlp/core.async-helpers "3.2.0-SNAPSHOT"] + [re-frame "1.1.2"]] - :dev + :profiles + {:dev {:dependencies - [[com.bhauman/figwheel-main "0.2.7"]] + [[com.bhauman/figwheel-main "0.2.7"] + [org.clojure/core.async "1.3.610"] + [net.clojars.jtkdvlp/re-frame-async-coeffects "2.0.0"]] :source-paths ["dev"]} @@ -46,6 +47,9 @@ [cider.piggieback/wrap-cljs-repl] :init-ns - user}}} + user + + :init + (fig-init)}}} ,,,) diff --git a/src/jtk_dvlp/re_frame/tasks.cljs b/src/jtk_dvlp/re_frame/tasks.cljs index ee603ca..89bb022 100644 --- a/src/jtk_dvlp/re_frame/tasks.cljs +++ b/src/jtk_dvlp/re_frame/tasks.cljs @@ -1,5 +1,7 @@ (ns jtk-dvlp.re-frame.tasks (:require + [cljs.core.async] + [jtk-dvlp.async :as a] [re-frame.core :as rf] [re-frame.interceptor :as interceptor])) @@ -7,60 +9,209 @@ ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Functions -(defn ->task - [data] - (assoc data ::id (random-uuid))) - (defn register + "Register task within app state. Also see event `::register`. + Tasks can be used via subscriptions `::tasks` and `::running?`." [db {:keys [::id] :as task}] (assoc-in db [::db :tasks id] task)) (defn unregister - [db id-or-task] - (let [id (or (::id id-or-task) id-or-task)] - (update-in db [::db :tasks] dissoc id))) + "Unregister task within app state. Also see event `::unregister`. + Tasks can be used via subscriptions `::tasks` and `::running?`." + [db {:keys [::id]}] + (update-in db [::db :tasks] dissoc id)) + +(def ^:private !global-default-completion-keys + (atom #{:on-complete :on-success :on-failure :on-error})) + +(def set-global-default-completion-keys! + "Sets global completion keys." + (partial reset! !global-default-completion-keys)) + +(def merge-global-default-completion-keys! + "Merge global completion keys." + (partial swap! !global-default-completion-keys merge)) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Interceptors +(defn- fx-handler-run? + [{:keys [stack]}] + (->> stack + (filter #(= :fx-handler (:id %))) + (seq))) + +(defn- normalize-task + [name-or-task] + (if (map? name-or-task) + name-or-task + {::name name-or-task})) + +(defn- normalize-effect-key + [effect-key] + (let [[effect :as effect-key] + (cond-> effect-key + (not (vector? effect-key)) + (vector))] + + (cond-> effect-key + (= effect :fx) + (conj 1)))) + +(defn- normalize-fx + [fx] + (cond + (keyword? fx) + {:effect-key (normalize-effect-key fx) + :completion-keys @!global-default-completion-keys} + + (vector? fx) + {:effect-key (normalize-effect-key (first fx)) + :completion-keys (some-> (next fx) (into #{}))} + + :else + {:effect-key (normalize-effect-key (:effect-key fx)) + :completion-keys (:completion-keys fx)})) + +(defonce ^:private !task<->fxs-counters + (atom {})) + +(defn- unregister-by-fx + [effect completion-keys task] + (reduce + (fn [effect completion-key] + (update effect completion-key (partial vector ::unregister-and-dispatch-original task))) + effect + completion-keys)) + +(defn- unregister-for-fx + [effects completion-keys task] + (mapv + (fn [[effect-key effect-value]] + [effect-key (unregister-by-fx effect-value completion-keys task)]) + effects)) + +(defn- unregister-by-fxs + [{:keys [effects] :as context} + {:keys [::id] :as task} + fxs] + + (loop [n 0 + + [{:keys [effect-key completion-keys]} & rest-fxs] + fxs + + {:key [effects] :as context} + context] + + (let [effect (get-in effects effect-key)] + (if effect-key + (->> task + (unregister-by-fx effect completion-keys) + (assoc-in context (cons :effects effect-key)) + (recur (inc n) rest-fxs)) + + (when (> n 0) + (swap! !task<->fxs-counters assoc id n) + context))))) + +(defn- unregister-by-failed-acofx + [context task ?acofx] + (cljs.core.async/take! + ?acofx + (fn [result] + (when (a/exception? result) + (rf/dispatch [::unregister task])))) + context) + +(defn- get-db + [context] + (or + (interceptor/get-effect context :db) + (interceptor/get-coeffect context :db))) + +(defn- includes-acofxs? + [context] + (contains? context :acoeffects)) + +(defn- handle-acofx-variant + [{:keys [acoeffects] :as context} task fxs] + (let [db + (get-db context) + + {:keys [dispatch-id ?error]} + acoeffects + + task + (assoc task ::id dispatch-id)] + + (if (fx-handler-run? context) + (or + (unregister-by-fxs context task fxs) + (interceptor/assoc-effect context :db (unregister db task))) + (-> context + (interceptor/assoc-effect :db (register db task)) + (unregister-by-failed-acofx task ?error))))) + +(defn- handle-straight-variant + [context task fxs] + (let [db + (get-db context) + + task + (assoc task ::id (random-uuid))] + + ;; NOTE: no need to register task in every case. the task register + ;; would be effectiv too late after finish the handler. + (if-let [context (unregister-by-fxs context task fxs)] + (interceptor/assoc-effect context :db (register db task)) + context))) + +(defn- get-original-event + [context] + (get-in context [:coeffects :original-event])) + +(defn- task-by-original-event + [context] + (-> context + (get-original-event) + (first))) + (defn as-task - ([effect-key] - (as-task effect-key :on-completed)) + "Creates an interceptor to mark an event as task. + Give it a name of the task or map with at least a `::name` key or nil / nothing to use the event name. + Tasks can be used via subscriptions `::tasks` and `::running?`. - ([effect-key & on-completion-keys] - (rf/->interceptor - :id - :as-task + Given vector `fxs` will be used to identify effects to monitor for the task. Can be the keyword of the effect or an vector of effect keyword or effect path (to handle special :fx effect) and completion keywords to hang in. Completion keys defaults to `:on-complete`, `:on-success`, `on-failure` and `on-error`. See also `set-global-default-completion-keys!` and `merge-global-default-completion-keys!`. - :after - (fn [context] - (let [effect - (rf/get-effect context effect-key) + Works in combination with https://github.com/jtkDvlp/re-frame-async-coeffects. For async coeffects there is no need to define what to monitor. Coeffects will be monitored automatically." + ([] + (as-task nil)) - task-id - (::id effect (random-uuid)) + ([name-or-task] + (as-task name-or-task nil)) - task - (assoc effect ::id task-id, ::effect effect-key) + ([name-or-task fxs] + (let [fxs (map normalize-fx fxs)] + (rf/->interceptor + :id + :as-task - effect' - (->> on-completion-keys - (map #(->> (get effect %) - (vector ::unregister-and-dispatch-original task-id))) - (zipmap on-completion-keys) - (merge effect)) + :after + (fn [context] + (let [task + (-> name-or-task + (or (task-by-original-event context)) + (normalize-task) + (assoc ::event (get-original-event context)))] - db' - (-> (rf/get-effect context :db) - (or (rf/get-coeffect context :db)) - (register task))] + (cond + (includes-acofxs? context) + (handle-acofx-variant context task fxs) - (if effect - (-> context - (interceptor/assoc-effect :db db') - (interceptor/assoc-effect effect-key effect')) - context)))))) + :else + (handle-straight-variant context task fxs)))))))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -71,14 +222,23 @@ (register db task))) (rf/reg-event-db ::unregister - (fn [db [_ id-or-task]] - (unregister db id-or-task))) + (fn [db [_ task]] + (unregister db task))) (rf/reg-event-fx ::unregister-and-dispatch-original - (fn [{:keys [db]} [_ id-or-task original-event-vec & original-event-args]] - (cond-> {:db (unregister db id-or-task)} - original-event-vec - (assoc :dispatch (into original-event-vec original-event-args))))) + (fn [_ [_ task original-event-vec & original-event-args]] + {::unregister-and-dispatch-original [task original-event-vec original-event-args]})) + +(rf/reg-fx ::unregister-and-dispatch-original + (fn [[{:keys [::id] :as task} original-event-vec original-event-args]] + (when original-event-vec + (rf/dispatch (into original-event-vec original-event-args))) + + (if (= 1 (get @!task<->fxs-counters id)) + (do + (swap! !task<->fxs-counters dissoc id) + (rf/dispatch [::unregister task])) + (swap! !task<->fxs-counters update id dec)))) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -95,7 +255,7 @@ (rf/reg-sub ::running? :<- [::tasks] - (fn [tasks [_ ids]] - (if ids - (->> tasks (keys) (filter ids) (seq) (some?)) - (->> tasks (seq) (some?))))) + (fn [tasks [_ name]] + (if name + (->> tasks (vals) (filter #(= (::name %) name)) (first) (some?)) + (-> tasks (first) (some?)))))