Liking cljdoc? Tell your friends :D

id: intro title: Introduction to GX sidebar_label: "Introduction" slug: /

GX Banner

Introduction

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.

Status

We say beta, but most things are stabilized.

Install

Leiningen:

[kepler16/gx.cljc "2.x.x"]

Deps:

{:kepler16/gx.cljc {:mvn/version "2.x.x"}}

Usage

To start using GX you need two things:

  • Graph - where nodes of our particular graph are defined
  • Graph Context - where signals, normalisation hints and other config is defined. This is optional, GX comes with sane defaults.

Default Graph Context

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.

Graph

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 Vincent
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close