core.async channel based event bus for Clojure(Script)
Main motivations for writing this library are:
- Based on core.async channel
- Fine grain event listening more than core.async pub/sub
- Request-reply communication between event emitter and listener
- Available on both Clojure and ClojureScript
[jp.nijohando/event "0.1.5"]
jp.nijohando/event {:mvn/version "0.1.5"}
Clojure
(require '[jp.nijohando.event :as ev]
'[clojure.core.async :as ca])
CloureScript
(require '[jp.nijohando.event :as ev :include-macros true]
'[clojure.core.async :as ca :include-macros true])
An event is expressed as a map. It can be created in literals or by function event
.
(ev/event "/messages/10/delete")
;;=> {:path "/messages/10/delete"}
(ev/event "/messages/post"
{:name "taro"
:text "hello!"})
;;=> {:path "/messages/post"
;; :value {:name "taro"
;; :text "hello!"}}
An event has three keys path
, header
and value
path
is set of a resource and operation, which is similar to the path of RESTful API, but it may contain not only a noun but also a verb.
header
is Control parameters which is appended by the library.
value
is body data of the event. It's optional depends on event type.
Function bus
creates an event bus that is a main line for propagating events.
(def bus (ev/bus))
Function emitize
connects the channel to the bus as an emitter channel.
(def emitter (ca/chan))
(ev/emitize bus emitter)
Writing an event to the channel, the event is emitted to the bus.
(ca/go
(ca/>! emitter {:path "/messages/post"
:value {:name "taro"
:text "hello!"}}))
Function listen
connects the channel to the bus as a listener channel and listens to events matching the specified path.
(def listener (ca/chan))
(ev/listen bus "/messages/post" listener)
Events matching the path can be read from the channel.
(ca/go-loop []
(when-some [{:keys [path value] :as event} (ca/<! listener)]
(println "from:" (:name value) "msg:" (:text value)))
(recur))
Path can be specified by metosin/reitit route syntax (internaly uses reitit-core)
;; Listen any message id's delete event
(ev/listen bus "/messages/:id/delete" listener)
;; Listen any message id's any event
(ev/listen bus "/messages/:id/*" listener)
;; Listen multiple type of events
(ev/listen bus ["/messages"
["/post"]
["/:id/delete"]] listener)
Matched route information is added to the header of the read event.
{:path "/messages/1/delete"
:header {:route #reitit.core.Match{:template "/messages/:id/delete",
:data {}
:result nil
:path-params {:id "1"}
:path "/messages/1/delete"}}}
Normaly, emitting event is one-way communication, but the emitter can also receive the reply event.
Each emitter channel has a unique id and endpoint path /emitters/:id/reply
to receive a reply.
Function emitize
can connect the channels to the bus as an emitter and a reply channel.
(def emitter (ca/chan))
(def reply-ch (ca/chan))
(ev/emitize bus emitter reply-ch)
A reply can be read from the reply channel.
;; Emit an event and listen the reply
(ca/go
(let [emitter (ca/chan)
reply-ch (ca/chan)]
(ev/emitize bus emitter reply-ch)
(ca/>! emitter (ev/event "/messages/post"))
(when-some [{:keys [value]} (ca/<! reply-ch)]
(prn "message" (:msg-id value) "created!"))))
Function reply-to
creates a reply event for the source event and it can be emitted via emitter channel.
;; Listen an event and emit the reply
(ca/go
(let [emitter (ca/chan)
listener (ca/chan)
msg-id (atom 0)]
(ev/emitize bus emitter)
(ev/listen bus "/messages/post" listener)
(ca/go-loop []
(when-some [event (ca/<! listener)]
(let [reply (ev/reply-to event {:status :created
:msg-id (swap! msg-id inc)})]
(ca/>! emitter reply)
(recur))))))
When an emitter channel is created per request (emitting an event), It becomes the request-reply pattern because the emitter-id has a unique value for each request.
© 2018 nijohando
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.