A Clojure library for state lifecycle-management using resettable delays, inspired by mount-lite, decomplected from any methodology.
With this library you create first class State objects. Think of them as Clojure's Delay objects, but resettable and tracked.
Let's create two State objects for our examples. First we need to require the small API of redelay:
(require '[redelay.core :refer [state status stop]])
Next we use the state
macro to create two State objects.
(def config
(state (println "Loading config...")
(edn/read-string (slurp "config.edn")))
(def db
(state :start ; <-- optional in this position
(println "Opening datasource...")
(hikari/make-datasource (:jdbc @config))
:stop
(println "Closing datasource...")
(hikari/close-datasource this)))
Let's quickly inspect one of the State objects we have just created:
config
;=> #<State@247136[user/state--312]: :not-delivered>
(realized? config)
;=> false
There are several things to note here.
:stop
expression is optional. Actually, all expressions to state
are optional.do
.state
body are considered to be the :start
expression, if not qualified otherwise.:stop
expression has access to a this
parameter, bound to the State value.realized?
on a State object, just like you can on a Delay.Now let's use our states.
Just like a Delay, the first time a State is consulted by a deref
(or force
), it is realized.
This means that the :start
expression is executed and its result is cached.
@db
Loading config...
Opening datasource...
;=> org.postgresql.ds.PGSimpleDataSource@267825
@db
;=> org.postgresql.ds.PGSimpleDataSource@267825
(realized? config)
;=> true
In the example you can see that the :start
expressions of the states are only executed once.
Subsequent derefs return the cached value.
A State implements Java's Closeable
, so you could call .close
on it.
This will execute the :stop
expression and clear its cache.
Now the State is ready to be realized again.
However, redelay keeps track of which states are realized and thus active.
You can see which states are active by calling (status)
:
(status)
;=> (#<State@247136[user/state--312]: {:jdbc-url "jdbc:postgresql:..."}>
;=> #<State@329663[user/state--315]: org.postgresql.ds.PGSimpleDataSource@267825>)
Because the active states are tracked, you can easily stop all of them by calling (stop)
.
All the active states are stopped (i.e. closed), in the reverse order of their realization.
(stop)
Closing datasource...
;=> (#<State@329663[user/state--315]: :not-delivered>
;=> #<State@247136[user/state--312]: :not-delivered>)
So no matter where your state lives, you can reset it and start afresh.
Oh and if a State's stop expression has a bug or can't handle its value, you can always force it to close with close!
.
Next to the :start
and :stop
expressions, you can also pass a :name
to the state
macro.
This makes recognizing the State objects easier.
The :name
expression must be a symbol.
(def config (state (load-config) :name user/config))
;=> #'user/config
config
;=> #<State@19042[user/config]: :not-delivered>
If you bind your State objects to a global var, it is common to have the name to be equal to the var it is bound to. Therefore the above can also be written as follows:
(defstate config (load-config))
Users of mount or mount-lite will recognize above syntax.
Trying to redefine a defstate
which is active (i.e. realized) is skipped and yields a warning.
The defstate
macro fully supports metadata on the name, a docstring and an attribute map (just like defn
).
Note that this metadata is set on the var.
If you want metadata on a State, you can use a :meta
expression in the body of the state
macro, or use Clojure's with-meta
on it.
Next to metadata support, Clojure's namespace
and name
functions also work on State objects.
For example, this may yield an easier to read status list:
(map name (status))
;=> ("config")
Since state in redelay is handled as first class objects, there are all kinds of testing strategies. It all depends a bit on where you keep your State objects (discussed in next section).
For the examples above you can simply use plain old with-redefs
to your hearts content.
We can redefine "production" states to other states, or even to a plain delay
.
There is no need for a special API to support testing.
For example:
(deftest test-in-memory
(with-redefs [config (delay {:jdbc-url "jdbc:derby:..."})]
(is (instance? org.apache.derby.jdbc.ClientDataSource @db))))
In some situations it might be a good idea to add a fixture to your tests, ensuring (stop)
is always called after a test.
Another option would be to use Clojure's with-open
, since State objects implement Closeable
:
(deftest test-in-memory
(with-open [config (make-config-state :test-env)
db (make-db-state @config)]
(is (instance? org.apache.derby.jdbc.ClientDataSource @db))))
Again, these are just examples. You may structure and use your State objects differently.
Although the examples above have bound the State objects to global vars, this is certainly not required. State objects can live anywhere and can be passed around like any other object. If you prefer a map of states for example, be it unrealized, realized or dereferenced, then that's perfectly feasible as well.
Because of its first class and unassuming nature, this library supports the whole spectrum of mount-like global states to Component-like system maps to Integrant-like data-driven approaches. This is also the reason that redelay does not have some sort of "start" or "init" function. You can easily add this to your application yourself, if you don't want to rely on derefs alone.
By the way, if you prefer system maps, have a look at the rmap library, as it combines well with redelay.
The redelay library is minimal on purpose.
It offers a complete first class State object and the two basic management functions (status)
and (stop)
.
Those two functions are actually implemented using the library's extension point: the watchpoint.
The library contains a public watchpoint
var.
You can watch this var by using Clojure's add-watch
.
The registered watch functions receive started (realized) State objects as "new" and stopped (closed) State object as "old".
You can do all kinds of things with this watchpoint, such as logging or keeping track of States yourself. You want to have more sophisticated stop logic with separate buckets/systems of states using their metadata for example? Go for it, be creative and use the library's building blocks to fit your perfect workflow!
That's it for simple lifecycle management around the stateful parts of your application. Have fun! 🚀
Copyright © 2020 Functional Bytes
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close