GX is data driven directed acyclic graph of state machines with configurable signals and nodes for Clojure(Script). Common usage - simple system wide component based dependency injection mechanism for startup/teardown.
We say beta, but most things are stabilized.
Leiningen:
[kepler16/gx.cljc "2.x.x"]
Deps:
{:kepler16/gx.cljc {:mvn/version "2.x.x"}}
To start using GX you need two things:
Context is a simple map with signals.
;; default context from gx
(def default-context
{;; initial state of nodes
:initial-state :uninitialised
:normalize {;; signal, which is default for all nodes
:auto-signal :gx/start
;; by top level graph props are pushed down to :gx/start
:props-signals #{:gx/start}}
;; used for plugging in third party components with other signal names
:signal-mapping {}
;; list of signals
:signals {:gx/start {;; from which states signal can be called
:from-states #{:stopped :uninitialised}
;; state of node after success signal
:to-state :started}
:gx/stop {:from-states #{:started}
:to-state :stopped
;; this is used as a sign of anti-signal and aplies
;; it in reversed order
:deps-from :gx/start}}})
There must be one (and only one) signal, which runs on from initial state. It is called a auto signal. In our case its :gx/start
.
The Graph is a plain Clojure map with defined nodes on the root level.
Lets create a graph of three nodes. Node value can be any data structure, primitive value, function call, or gx reference (gx/ref
, gx/ref-keys
):
(def fancy-graph
{:user/data {:name "Angron"
:also-named "Red Angel"
:spoken-language "Nagrakali"
:side :chaos}
;; unqualified Clojure(Script) core functions, fully qualified
;; functions and gx refs will be resolved by GX
;; special forms and macros are not supported (e.g. throw, if, loop etc)
:user/name '(get (gx/ref :user/data) :name)
:user/lang '(get (gx/ref :user/data) :spoken-language)})
Here we have a node :user/data
and two dependent nodes :user/name
and :user/lang
. The next step is normalization. It is a process of converting your graph to a nodes of state machine components, signal receivers:
;; not mandatory, happens automatically during signal execution
;; returns gx-map (a system) - a map with following keys:
;; - :initial-graph - in our case fancy-graph
;; - :graph - normalized graph
;; - :failures - list of humanized error messages (if any)
;; - :context - current GX context
(def system (gx/normalize {:graph fancy-graph}))
Here is internals of our normalized system:
{:graph
#:user{:data
#:gx{:start
#:gx{;; autogenerated processor
:processor <...>/auto-signal-processor],
;; signal dependenies
:deps #{},
;; signal resolved dependencies
:resolved-props {}},
:stop
#:gx{:processor :value,
:resolved-props nil,
:resolved-props-fn nil,
:deps #{}},
;; state of node
:state :uninitialised,
;; value of node
:value nil,
;; type of node. :static for value components
;; and :component for custom components
:type :static,
;; nomralization flag
:normalized? true},
:name
#:gx{:start
#:gx{:processor <...>/auto-signal-processor],
:deps #{:user/data},
:resolved-props #:user{:data (gx/ref :user/data)}},
:stop
#:gx{:processor :value,
:resolved-props nil,
:resolved-props-fn nil,
:deps #{}},
:state :uninitialised,
:value nil,
:type :static,
:normalized? true},
:lang
#:gx{:start
#:gx{:processor <...>/auto-signal-processor],
:deps #{:user/data},
:resolved-props #:user{:data (gx/ref :user/data)}},
:stop
#:gx{:processor :value,
:resolved-props nil,
:resolved-props-fn nil,
:deps #{}},
:state :uninitialised,
:value nil,
:type :static,
:normalized? true}},
:context
{:initial-state :uninitialised,
:normalize {:auto-signal :gx/start, :props-signals #{:gx/start}},
:signal-mapping {},
:signals
#:gx{:start {:from-states #{:stopped :uninitialised}, :to-state :started},
:stop
{:from-states #{:started}, :to-state :stopped, :deps-from :gx/start}}},
:initial-graph
#:user{:data
{:name "Angron",
:also-named "Red Angel",
:spoken-language "Nagrakali",
:side :chaos},
:name (get (gx/ref :user/data) :name),
:lang (get (gx/ref :user/data) :spoken-language)}}
Now every node is in a normalized state. It has a startup signal :gx/start
but not :gx/stop
, because we didn't define any signals on nodes.
Next, we send a signal to our graph by calling gx/signal
. Signals run asynchronously (using funcool/promesa) and returns resolvable object:
(def started @(gx/signal system :gx/start))
Value in the started
variable is a system with the new state or with same state and list of failures (if any). GX itself does not store graphs, it simply returns new graphs on every signal. Managing graph stores should happen on the application side or by using helper namespace k16.beta.gx.system
.
Here are some utility functions to view the internals of the graph:
(gx/system-value started)
;; => #:user{:data
;; {:name "Angron",
;; :also-named "Red Angel",
;; :spoken-language "Nagrakali",
;; :side :chaos},
;; :name "Angron",
;; :lang "Nagrakali"}
(gx/system-state started)
;; => #:user{:data :started, :name :started, :lang :started}
(gx/system-failure system)
;; => #:user{:data nil, :name nil, :lang nil}
Tutorial contains a more practical example.
Can you improve this documentation? These fine people already did:
Artem Medeusheyev & Alexis VincentEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close