std.concurrent.request
single, bulk, asynchronous, and transactional request orchestration
Define one client request protocol and reuse it across synchronous calls, futures, batching, transformations, and transactions.
1 Overview
Request clients implement std.protocol.request/IRequest. The orchestration layer prepares options, invokes single or bulk transport functions, processes outputs, and applies pre, post, chain, timing, and error handlers. Plain functions also implement the request protocol.
2 Walkthrough
2.1 Make a single request
(require '[std.concurrent.request :as request])
;; Functions are valid clients; each command is an argument vector.
(request/req + [1 2 3])
(request/req + [1 2 3]
{:pre [(fn [args] (conj args 4))]
:post [inc]
:chain [str]})
2.2 Run asynchronously
Set :async to receive a future. :measure can be a callback, atom, or map of start, stop, and timer handlers.
(def pending
(request/req + [1 2 3]
{:async true
:measure println}))
@pending
2.3 Batch and transact
(request/req:bulk [+ {:async false}]
(request/req + [1 2])
(request/req + [3 4]))
(request/req:transact [+ {}]
(request/req + [1 2])
(request/req + [3 4]))
(request/bulk:map + request/req-fn [[1 2] [3 4]])
3 API
- *bulk*
- *current*
- *debug*
- *inputs*
- *transact*
- bulk
- bulk-collect
- bulk-context
- bulk-process
- bulk:inputs
- bulk:map
- bulk:transact
- fn-process-bulk
- fn-process-single
- fn-request-bulk
- fn-request-single
- measure:debug
- opts:timer
- opts:wrap-measure
- req
- req-fn
- req.fn
- req.more
- req.time
- req:bulk
- req:bulk.debug
- req:in
- req:opts
- req:opts-clean
- req:opts-init
- req:return
- req:single
- req:single-complete
- req:single-prep
- req:transact
- req:unit
- transact
- transact-context
- transact-prep
- transact:map
v 3.0
(defn bulk
([client thunk]
(bulk client thunk {}))
([client thunk opts]
(let [{:keys [async catch chain post] :as opts} (req:opts-init opts)
[bulk current] (bulk-collect client thunk chain)
received (f/on:all current
(fn [& arr]
(reduce h/call (apply vector arr) post)))
received (cond-> received
catch (f/on:exception catch))
final (f/future:chain received chain)
_ (cond bulk
(bulk-process client bulk opts)
:else
(swap! (get *current* client) conj final))]
(req:return final (boolean (or (not bulk)
(get *transact* client)
async))))))
link
(bulk |client| (fn [] (req-fn |client| {:type :eval, :form '(+ 1 2 3)}) (req-fn |client| {:type :eval, :form '(+ 4 5 6)}))) => [6 15] (bulk |client| (fn [] (req-fn |client| {:type :eval, :form '(+ 1 2 3)}) (req-fn |client| {:type :eval, :form '(+ 4 5 6)})) {:chain [#(apply + %)]}) => 21 (bulk |client| (fn [] (req-fn |client| {:type :eval, :form 1} {:chain [inc]}) (req-fn |client| {:type :eval, :form 2} {:chain [inc]}))) => [2 3] (bulk |client| (fn [] (bulk |client| (fn [] (req-fn |client| {:type :eval, :form 1} {:chain [inc]}) (req-fn |client| {:type :eval, :form 2} {:chain [inc]}))) (req-fn |client| {:type :eval, :form 3} {:chain [inc]}) (req-fn |client| {:type :eval, :form 4} {:chain [inc]}))) => [[2 3] 4 5] (bulk |client| (fn [] (bulk |client| (fn [] (req-fn |client| {:type :eval, :form 1} {:chain [inc]}) (req-fn |client| {:type :eval, :form 2} {:chain [inc]})) {:chain [#(apply + %)]}) (req-fn |client| {:type :eval, :form 3} {:chain [inc]}) (req-fn |client| {:type :eval, :form 4} {:chain [inc]})) {:chain [#(apply + %)]}) => 14
v 3.0
(defn bulk-collect
([client thunk chain]
(let [top (get *bulk* client)
context (or top (bulk-context))
current (atom [])]
(binding [*bulk* (assoc *bulk* client context)
*current* (assoc *current* client current)]
(thunk)
[(if (nil? top)
(-> (dissoc context :cache)
(merge @(:cache context))))
@current]))))
link
(-> (bulk-collect |client| (fn [] (req:unit |client| {:type :eval :form '(+ 1 2 3)}) (req:unit |client| {:type :eval :form '(+ 4 5 6)})) []) first :inputs) => [{:type :eval, :form '(+ 1 2 3)} {:type :eval, :form '(+ 4 5 6)}]
v 3.0
(defn bulk-context
([]
{:received (f/incomplete)
:cache (atom {:inputs []
:outputs []})}))
link
(bulk-context) => map?
bulk-process ^
[client {:keys [inputs received]} {:keys [pre], :as opts}]
processes the client given bulk inputs
v 3.0
(defn bulk-process
([client {:keys [inputs received]} {:keys [pre] :as opts}]
(let [result (h/->> (reduce h/call inputs pre)
(request-bulk client % opts)
(process-bulk client inputs % opts))
_ (f/future:force received result)]
received)))
link
@(bulk-process |client| {:inputs [{:type :eval, :form '(+ 1 2 3)} {:type :eval, :form '(+ 4 5 6)}] :received (future/incomplete)} {}) => [6 15]
v 3.0
(defn bulk:inputs
([client thunk]
(let [context (bulk-context)]
(binding [*bulk* (assoc *bulk* client context)
*current* (assoc *current* client (atom []))]
(thunk)
(:inputs @(:cache context))))))
link
(bulk:inputs |client| (fn [] (req:unit |client| {:type :eval :form '(+ 1 2 3)}) (req:unit |client| {:type :eval :form '(+ 4 5 6)}))) => [{:type :eval, :form '(+ 1 2 3)} {:type :eval, :form '(+ 4 5 6)}]
v 3.0
(defn bulk:map
([client f inputs]
(bulk:map client f inputs {}))
([client f inputs opts]
(bulk client
(fn []
(mapv (partial f client) inputs))
opts)))
link
(bulk:map |client| req-fn [{:type :eval :form 1} {:type :eval :form 2}]) => [1 2]
bulk:transact ^
[client thunk] [client thunk opts]
creates a transaction within a bulk context
v 3.0
(defn bulk:transact
([client thunk]
(bulk:transact client thunk {}))
([client thunk opts]
(let [{:keys [async chain pre post] :as opts} (req:opts-init opts)
state (volatile! nil)
_ (reduce h/call nil pre)]
(bulk client
(fn []
(let [res (transact client thunk)]
(vreset! state res)))
(req:opts-clean opts))
(-> @state
(f/on:all (fn [& arr]
(reduce h/call (apply vector arr) post)))
(f/future:chain chain)
(req:return (boolean (or (get *bulk* client)
(get *transact* client)
async)))))))
link
(bulk:transact |client| (fn [] (req |client| {:type :eval :form 1} {:chain [inc]}) (req |client| {:type :eval :form 1} {:chain [#(* 2 %)]}))) => [2 2]
NONE
(defn- fn-request-bulk
([f arr _]
(mapv (partial apply f) arr)))
link
v 3.0
(defn measure:debug
([{:keys [context]}]
{:start (fn [input]
(env/local :println :START
context input))
:stop (fn [output]
(env/local :println :STOP
context output))}))
link
(measure:debug {}) => (contains {:start fn? :stop fn?})
v 3.0
(defn opts:timer
([timer]
(let [timer (cond (h/atom? timer)
(partial reset! timer)
:else timer)
timer (if (fn? timer)
(doto (f/incomplete)
(f/on:success timer))
timer)]
timer)))
link
(opts:timer prn) => future/future?
v 3.0
(defn opts:wrap-measure
([opts measure]
(let [{:keys [start stop timer]
:or {start h/NIL
stop h/NIL}} (cond (map? measure)
measure
:else {:timer measure})
timer (opts:timer timer)
holder (atom nil)
start-fn (fn [input]
(start input)
(reset! holder (t/time-ns))
input)
stop-fn (fn [output]
(if timer
(f/future:force timer
{:output output
:start @holder
:end (t/time-ns)}))
(stop output)
output)]
(-> opts
(update :pre (comp vec (partial cons start-fn)))
(update :chain (fnil #(conj % stop-fn) []))))))
link
(opts:wrap-measure {} prn) => (contains-in {:pre [fn?], :chain [fn?]})
req ^
[client command] [client command opts]
execute command on single, bulk and transact calls
v 3.0
(defmacro req
([client command]
(let [context (-> (meta &form)
(assoc :ns (env/ns-sym)))]
`(req-fn ~client ~command {:context (quote ~context)})))
([client command opts]
(let [context (-> (meta &form)
(assoc :ns (env/ns-sym)))]
`(req-fn ~client ~command (assoc ~opts :context (quote ~context))))))
link
(req |client| {:type :eval :form 1}) => 1 (req |client| {:type :transact/start}) => :transact/start (req |client| {:type :eval :form 1}) => :transact/queued (req |client| {:type :eval :form 1}) => :transact/queued (req |client| {:type :transact/end}) => [1 1]
v 3.0
(defn req-fn
([client command]
(req-fn client command {}))
([client command opts]
(let [bulk (get *bulk* client)
transact (get *transact* client)
opts (req:opts-init opts)
opts (if transact
(transact-prep client transact command opts)
opts)]
(cond *inputs*
(swap! *inputs* conj command)
bulk
(req:unit client command opts)
:else
(req:single client command opts)))))
link
(binding [*inputs* (atom [])] (req-fn :client {:type :eval :form 1}) @*inputs*) => [{:type :eval :form 1}]
link
(req + [1 2 3 4]) => 10 @(req + [1 2 3 4] {:async true}) => 10
link
(req-fn |client| {:type :eval :form 1} {:post [inc inc]}) => 3 (bulk |client| (fn [] (req-fn |client| {:type :eval :form 1} {:post [inc inc]}))) => [3] (bulk |client| (fn [] (req-fn |client| {:type :eval :form 1} {:pre [#(update % :form inc)] :post [inc]}))) => [3] (bulk |client| (fn [] (req-fn |client| {:type :eval :form 1} {:pre [#(update % :form inc)] :post [inc] :chain [inc]}))) => [4] (->> (transact |client| (fn [] (req-fn |client| {:type :eval :form 1} {:pre [#(update % :form inc)] :post [inc] :chain [inc]}))) (map deref)) => [4]
link
;; ;; Manual ;; (let [state (atom {}) _ (req-fn |client| {:type :eval :form 1} {:pre [(fn [x] (swap! state assoc :start (time/time-ns)) x)] :post [(fn [x] (swap! state assoc :end (time/time-ns)) x)]}) {:keys [start end]} @state] (time/format-ns (- end start))) => string? ;; ;; With measure key ;; (def -p- (promise)) (req-fn |client| {:type :eval :form 1} {:measure #(deliver -p- %)}) @-p- ;; {:output 1, :start 1600748490664158898, :end 1600748490664239782} => (contains {:start number? :end number? :output 1})
v 3.0
(defmacro req:bulk
([[client opts] & body]
(let [context (-> (meta &form)
(assoc :ns (env/ns-sym)
:type :bulk))]
`(bulk ~client
(fn []
~@body)
(assoc ~opts :context (quote ~context))))))
link
(req:bulk [+] (req + [1 2 3]) (req + [1 2 3]) (req - [4 5 6])) => [6 6]
link
(-> (req:bulk [+ {:debug true}] (req-fn + [1 2 3] {:debug true}) (req-fn + [1 2 3] {:debug true}) (req-fn - [4 5 6] {:debug true})) (env/with-out-str)) => string?
v 3.0
(defmacro req:in
([& body]
`(binding [*inputs* (atom [])]
((fn [] ~@body))
@*inputs*)))
link
(req:in (req nil [1]) (req nil [2]) (req nil [3])) => [[1] [2] [3]]
req:opts ^
[opts] [opts {:keys [pre post chain], :as m}]
clean opts for processing inputs
v 3.0
(defn req:opts
([opts]
(req:opts opts nil))
([opts {:keys [pre post chain] :as m}]
(cond-> opts
m (merge (dissoc m :chain :post :pre))
pre (update :pre #(vec (into % pre)))
post (update :post (comp vec (partial concat post)))
chain (update :chain (comp vec (partial concat chain))))))
link
(req:opts {} {}) => {}
v 3.0
(defn req:opts-clean
([opts]
(dissoc opts :measure :time :debug :pre :post
:chain :async :final :received :transact)))
link
(req:opts-clean {}) => {}
v 3.0
(defn req:opts-init
([{:keys [measure debug context] :as opts}]
(cond-> opts
debug (opts:wrap-measure (measure:debug opts))
measure (opts:wrap-measure measure))))
link
(let [opts (req:opts-init {:debug true :context {:line 1}})] [(= 1 (count (:pre opts))) (fn? (first (:pre opts))) (= 1 (count (:chain opts))) (fn? (first (:chain opts)))]) => [true true true true]
v 3.0
(defn req:return
([result async]
(cond async result
(f/future? result)
@result
:else
result)))
link
(req:return 1 false) => 1 (req:return (future/completed 1) false) => 1 (req:return (future/completed 1) true) => future/future?
v 3.0
(defn req:single
([client command]
(req:single client command {}))
([client command opts]
(let [{:keys [pre post final async] :as opts} (req:single-prep opts)
result (h/-> command
(reduce h/call % pre)
(request-single client % opts)
(process-single client % opts))
final (req:single-complete opts result)]
(req:return final (if (get *transact* client)
true
async)))))
link
(req:single |client| {:type :eval :form '(+ 1 2 3 4)}) => 10 @(req:single |client| {:type :eval :form '(+ 1 2 3 4)} {:async true}) => 10 (req:single |client| {:type :eval :form '(+ 1 2 3 4)} {:chain [inc inc]}) => 12 @(req:single |client| {:type :eval :form '(+ 1 2 3 4)} {:async true :chain [inc inc]}) => 12
req:single-complete ^
[{:keys [async transacted received final post chain], :as opts} result]
completes the `req:single` call
v 3.0
(defn req:single-complete
([{:keys [async transacted received final post chain] :as opts} result]
(cond transacted final
async
(do (f/future:force received
(reduce h/call result post))
final)
:else (h/-> result
(reduce h/call % post)
(reduce h/call % chain)))))
link
(req:single-complete (req:single-prep {}) 1) => 1 @(req:single-complete (req:single-prep {:async true}) 1) => 1
req:single-prep ^
[{:keys [async received transacted chain], :as opts}]
prepares `req:single` options
v 3.0
(defn req:single-prep
([{:keys [async received transacted chain] :as opts}]
(cond async
(let [received (or received (f/incomplete))
final (if transacted
transacted
(f/future:chain received chain))]
(assoc opts :received received :final final))
:else opts)))
link
(req:single-prep {:async true}) => map? (req:single-prep {:async true :transacted (future/incomplete)}) => map?
v 3.0
(defmacro req:transact
([[client opts] & body]
(let [context (-> (meta &form)
(assoc :ns (env/ns-sym)
:type :transact))]
`(bulk:transact ~client
(fn []
~@body)
(assoc ~opts :context (quote ~context))))))
link
(-> (req:transact [|client| {:debug true}] (req |client| {:type :eval :form 1} {:debug true}) (req |client| {:type :eval :form 2} {:debug true})) (env/with-out-str)) => string?
req:unit ^
[client command] [client command {:keys [pre post transacted catch chain], :as opts}]
creates a single request call with `*bulk*` context
v 3.0
(defn req:unit
([client command]
(req:unit client command {}))
([client command {:keys [pre post transacted catch chain] :as opts}]
(let [{:keys [received cache]} (get *bulk* client)
output (at/swap-return! cache
(fn [{:keys [inputs outputs] :as m}]
(let [i (count inputs)
output (f/on:success received
(fn [arr]
(reduce h/call (nth arr i) post)))
output (cond-> output
catch (f/on:exception catch))
final (if transacted
(f/future:chain transacted [])
(f/future:chain output chain))
command (if (h/iobj? command)
(vary-meta command merge (req:opts-clean opts))
command)]
[final (-> m
(update :inputs conj (reduce h/call command pre))
(update :outputs conj final))])))
_ (swap! (get *current* client) conj output)]
output)))
link
(binding [*bulk* {|client| (bulk-context)} *current* {|client| (atom [])}] (req:unit |client| {:type :eval :form '(+ 1 2 3)}) (-> (get *bulk* |client|) :cache deref :inputs)) => {:type :eval :form '(+ 1 2 3)}
v 3.0
(defn transact
([client thunk]
(let [top (get *transact* client)
context (or top (transact-context))
start-cmd (transact-start client)
end-cmd (transact-end client)
_ (if (nil? top)
(req-fn client start-cmd {:async true}))
results (binding [*transact* (assoc *transact* client context)]
(thunk)
(:outputs @context))
final (-> @context :inputs :final)
output (if (nil? top)
(req-fn client end-cmd {:async true}))
_ (if output
(f/on:complete output
(fn [val err]
(if err
(f/future:force final :exception err)
(f/future:force final :value val)))))]
results)))
link
(->> (transact |client| (fn [] (req |client| {:type :eval :form 1} {:chain [#(* 8 %)]}))) (map deref)) => '(8) ;; ;; bulk in transact ;; (->> (transact |client| (fn [] (bulk |client| (fn [] (transact |client| (fn [] (req |client| {:type :eval :form 1} {}))) (req |client| {:type :eval :form 2} {}))) (bulk |client| (fn [] (req |client| {:type :eval :form 3} {}) (req |client| {:type :eval :form 4} {}))))) (map deref)) => '(1 2 3 4) ;; ;; Nesting bulk and transact ;; (bulk |client| (fn [] (req |client| {:type :eval :form 1} {:chain [#(* 8 %)]}) (transact |client| (fn [] (req |client| {:type :eval :form 1} {:chain [#(* 2 %)]}) (req |client| {:type :eval :form 1} {:chain [#(* 2 %)]}))) (transact |client| (fn [] (bulk |client| (fn [] (req |client| {:type :eval :form 2} {:chain [#(* 2 %)]}))))) (transact |client| (fn [] (req |client| {:type :eval :form 3} {:chain [#(* 2 %)]}))))) => [8 :transact/start 2 2 [1 1] :transact/start [4] [2] :transact/start 6 [3]]
v 3.0
(defn transact-context
([]
(atom {:inputs {:final (f/incomplete)
:count 0}
:outputs []})))
link
(transact-context) => f/atom?
transact-prep ^
[client context command {:keys [post chain], :as opts}]
prepare req:opts given transaction context
v 3.0
(defn transact-prep
([client context command {:keys [post chain] :as opts}]
(let [transacted (at/swap-return! context
(fn [{:keys [inputs outputs] :as m}]
(let [{:keys [final count]} inputs
transacted (-> (f/on:success
final
(fn [arr]
(reduce h/call (nth arr count) post)))
(f/future:chain chain))]
[transacted (-> (update-in m [:inputs :count] inc)
(update :outputs conj transacted))])))]
(assoc opts :transacted transacted))))
link
(transact-prep |client| (transact-context) {:type :eval, :form 1} {:chain [inc]}) => map?
transact:map ^
[client f inputs] [client f inputs opts]
performs a transaction across the client
v 3.0
(defn transact:map
([client f inputs]
(transact:map client f inputs {}))
([client f inputs opts]
(bulk:transact client
(fn []
(mapv (partial f client) inputs))
opts)))
link
(transact:map |client| req-fn [{:type :eval :form 1} {:type :eval :form 2}]) => [1 2]