Yet another state management solution for Clojure applications
“Industrialization is the systemic exploitation of wasting assets. In all too many cases, the thing we call progress is merely an acceleration in the rate of that exploitation.” — Aldous Huxley
Clojure has many fantastic solutions for application state management - component, mount, and integrant are all fantastic libraries which bring unique and valuable features to the table. They serve as great inspiration
systemic
is similar to mount
, in that it strives to prioritize the
experience in the repl and makes use of clojure's own resolution
capabilities to implicitly define dependencies between components. Additionally
it uses dynamic scope to allow for multiple isolated systems in a single repl
allowing for testing of systems in the same repl as development.
(ns example.dep-resolution
(:require [systemic.core :as systemic :refer [defsys]]))
(defsys *port*
:start (read-string (System/getenv "APPLICATION_PORT")))
(defsys *server*
:start (start-web-server *port*)
:stop (shutdown-server *server*))
(defsys *monitor*
:extra-deps [*server*]
:start
(start-monitor!)
:stop
(kill-monitor!))
(systemic/start!)
;; => ('example.dep-resolution/*port* 'example.dep-resolution/*server* 'example.dep-resoltuion/*monitor*)
Using the systemic/with-system
creates an isolated environment where none of
the existing systems are running.
(defsys *api-key*
(->> (io/resource "secrets.edn")
(slurp)
(read-string)
:api-key))
(defn decorate-req
[req]
(-> req
(update :url #(str "https://api.example.com" %))
(assoc :as :json)
(assoc-in [:headers "API-KEY"] *api-key*)
(assoc-in [:headers "USer-Agent"] "Shiny Co. API Client")))
(defstate *send-req*
:extra-deps [*api-key*]
:start
(comp http/request decorate-req))
(defn fetch-users
[]
(:body (*send-req* {:method :get
:url "/users"})))
;; Example test
(deftest fetch-users-test
(let [req (atom nil)]
(with-system [*send-req* #(do (reset! req %)
{:body [{:name "Bob"}
{:name "Steve"}]})]
;; Note that because we have our own definition for `*send-req*` above,
;; dependencies (ie. `*api-key*`) of the original `*send-req*` will not be started.
(systemic/start! `*send-req*)
(is (= (fetch-users)
[{:name "Bob"}
{:name "Steve"}])))))
Systemic makes heavy use of dynamic scope and there are a few sharp edges to be
aware of when working with it. Dynamic bindings are thread-local, which means
that when you create a new thread you need to use bound-fn
to ensure that the
local bindings in the current thread make their way to the new thread.
Fortunately, most of clojure's built in concurrency primitives and libraries
handle this for you behind the scenes.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close