[mod] Simplify middleware - don't auto compose

Previously:

  It was possible to provide a vector of middleware fns when creating
  signals or adding handlers, e.g.:

    (event! ::ev-id1 {:middleware [fn1 fn2 ...]}),
    (add-handler! ::handler-id1 <handler-fn> {:middleware [fn1 fn2 ...]})

After this commit:

  Middleware is always expected to be a single fn, or nil.
  A `comp-middleware` util has been added to make it easy to compose multiple
  middleware fns into one.

Motivation:

  The previous (auto-composition) behaviour was nice when adding handlers,
  but incurred a (small but non-trivial) runtime cost when creating signals.

  The actual benefit (convenience) of auto-composition is very small, so
  I've decided to just remove this feature and add the `comp-middleware` util.

  Note that I ruled out the option of doing auto-comp only when adding handlers
  since that've meant an inconsistency and so complexity for little benefit.
This commit is contained in:
Peter Taoussanis 2024-04-29 23:39:33 +02:00
parent 63f488082b
commit 839143167b
4 changed files with 23 additions and 23 deletions

View file

@ -22,7 +22,7 @@ Signal options (shared by all signal creators):
`:sample-rate` - ?rate ∈ℝ[0,1] for signal sampling (0.75 => allow 75% of signals, nil => allow all)
`:when` -------- Arb ?form; when present, form must return truthy to allow signal
`:rate-limit` -- ?spec as given to `taoensso.telemere/rate-limiter`, see its docstring for details
`:middleware` -- ?[(fn [signal])=>modified-signal ...] signal middleware
`:middleware` -- Optional (fn [signal]) => ?modified-signal to apply when signal is created
`:trace?` ------ Should tracing be enabled for `:run` form?
<kvs> ---------- Other arb user-level ?kvs to incl. in signal. Typically NOT included in

View file

@ -72,6 +72,7 @@
enc/chance
enc/rate-limiter
enc/newline
enc/comp-middleware
impl/msg-splice
impl/msg-skip
@ -143,14 +144,13 @@
(comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*)))
;;;; Middleware
;;;; Signal middleware
(enc/defonce ^:dynamic *middleware*
"Optional vector of unary middleware fns to apply (sequentially/left-to-right)
to each signal before passing it to handlers. If any middleware fn returns nil,
aborts immediately without calling handlers.
"Optional (fn [signal]) => ?modified-signal to apply (once) when
signal is created. When middleware returns nil, skips all handlers.
Useful for transforming each signal before handling.
Compose multiple middleware fns together with `comp-middleware.
Re/bind dynamic value using `with-middleware`, `binding`.
Modify root (base) value using `set-middleware!`."
@ -159,13 +159,13 @@
#?(:clj
(defmacro set-middleware!
"Set `*middleware*` var's root (base) value. See `*middleware*` for details."
[root-val] `(enc/set-var-root! *middleware* ~root-val)))
[?root-middleware-fn] `(enc/set-var-root! *middleware* ~?root-middleware-fn)))
#?(:clj
(defmacro with-middleware
"Evaluates given form with given `*middleware*` value.
See `*middleware*` for details."
[init-val form] `(binding [*middleware* ~init-val] ~form)))
[?middleware-fn form] `(binding [*middleware* ~?middleware-fn] ~form)))
;;;; Signal creators
;; - signal! [ opts] ; => allowed? / run result (value or throw)

View file

@ -632,9 +632,9 @@
'~run-form ~'__run-result ~error-form)]
;; Final unwrapped signal value visible to users/handler-fns, allow to throw
(if-let [call-middleware# ~middleware-form]
((sigs/get-middleware-fn call-middleware#) ~'__signal) ; Can throw
(do ~'__signal)))))]
(if-let [sig-middleware# ~middleware-form]
(sig-middleware# ~'__signal) ; Apply signal middleware, can throw
(do ~'__signal)))))]
;; Could avoid double `run-form` expansion with a fn wrap (>0 cost)
;; (let [run-fn-form (when run-form `(fn [] (~run-form)))]

View file

@ -229,10 +229,10 @@
(testing "Call middleware"
(let [c (enc/counter)
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false}))
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}))
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware [(fn [_] "signal-value")]}))]
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false}))
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware (fn [_] "signal-value")}))]
[(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2}))
(is (= rv2 3)) (is (nil? sv2))
@ -260,31 +260,31 @@
(let [c (enc/counter)
sv-h1_ (atom nil)
sv-h2_ (atom nil)
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})]
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})]
;; Note that call middleware output is cached and shared across all handlers
(binding [impl/*sig-handlers* [wh1 wh2]]
(let [;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv1 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
rv1 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv1-h1 @sv-h1_
sv1-h2 @sv-h2_
c1 @c
;; 1x run
rv2 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false})
rv2 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false})
sv2-h1 @sv-h1_
sv2-h2 @sv-h2_
c2 @c ; 8
;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv3 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})
rv3 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv3-h1 @sv-h1_
sv3-h2 @sv-h2_
c3 @c ; 15
;; 4x handler middleware
rv4 (sig! {:level :info, :middleware [(fn [_] {:my-sig-val? true})]})
rv4 (sig! {:level :info, :middleware (fn [_] {:my-sig-val? true})})
sv4-h1 @sv-h1_
sv4-h2 @sv-h2_
c4 @c]
@ -321,7 +321,7 @@
(tel/with-handler :hid1
(fn [sv] (force (:data sv)) (reset! sv_ sv))
{:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil,
:middleware [(fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))]}
:middleware (fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))}
[(is (->> (sig! {:level :info, :when (ex1!)}) (throws? :ex-info "Ex1")) "`~filterable-expansion/allow` throws at call")
(is (->> (sig! {:level :info, :inst (ex1!)}) (throws? :ex-info "Ex1")) "`~inst-form` throws at call")
@ -339,7 +339,7 @@
(testing "Throwing call middleware"
(reset-state!)
[(is (true? (sig! {:level :info, :middleware [(fn [_] (ex1!))]})))
[(is (true? (sig! {:level :info, :middleware (fn [_] (ex1!))})))
(is (= @sv_ :nx))
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))])