Compare commits

..

No commits in common. "master" and "0.1.9" have entirely different histories.

56 changed files with 674 additions and 1192 deletions

View file

@ -1 +0,0 @@
{:config-paths ["../resources/clj-kondo.exports/mount/mount/"]}

6
.gitignore vendored
View file

@ -1,10 +1,7 @@
/target /target
/classes /classes
/checkouts /checkouts
.cpcache/ pom.xml
.rebel_readline_history
cljs-test-runner-out
node_modules
pom.xml.asc pom.xml.asc
.repl* .repl*
dev/resources/public/js/* dev/resources/public/js/*
@ -21,4 +18,3 @@ doo-index.html
/.nrepl-history /.nrepl-history
.cljs_rhino_repl/ .cljs_rhino_repl/
out/ out/
.clj-kondo/.cache

View file

@ -1,26 +0,0 @@
sudo: false
language: java
script:
- boot test
- boot test-cljs
- boot test-cljs-advanced
install:
- mkdir -p ~/bin
- export PATH=~/bin:$PATH
- curl -L https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh -o ~/bin/boot
- chmod +x ~/bin/boot
jdk: openjdk8
env:
matrix:
- BOOT_CLOJURE_VERSION=1.8.0
global:
- JAVA_OPTS="-Xms512m -Xmx2048m"
jdk:
- openjdk8
cache:
directories:
- $HOME/.m2
- $HOME/.boot/cache/bin
- $HOME/.boot/cache/lib
- $HOME/bin

View file

@ -1,58 +1,4 @@
## 0.1.19
###### Wed Aug 21 22:43:40 2024 -0400
* add `clojure-kondo`
## 0.1.17
###### Mon Dec 19 15:17:21 2022 -0500
* 2d050e9 fix: swap-states non string rollback (thanks to [egg-juxt](https://github.com/egg-juxt))
## 0.1.16
###### Mon Jan 28 10:34:26 2019 -0500
* fb52f79 prevent reloading of mount.core ns ([106](https://github.com/tolitius/mount/issues/106))
* c5f3e4c current-state should return Derefable on :cljc ([104](https://github.com/tolitius/mount/issues/104))
* c2687d1 silent `*logger*` warning in latest ClojureScript([101](https://github.com/tolitius/mount/issues/101))
* bb23747 switch form circle to travis
## 0.1.14
###### Thu Oct 25 18:34:22 2018 -0400
* cljs: throw js/Error not just a string ([#100](https://github.com/tolitius/mount/issues/100))
* add ^{:on-lazy-start :throw} ([#95](https://github.com/tolitius/mount/issues/95) and [#99](https://github.com/tolitius/mount/issues/99))
* self hosted ClojureScript support ([#85](https://github.com/tolitius/mount/issues/85) and [#97](https://github.com/tolitius/mount/issues/97))
## 0.1.12
###### Sat Feb 10 14:53:04 2018 -0500
* remove ref to old state alias in `swap-states` ([#89](https://github.com/tolitius/mount/issues/89))
* `:on-reload :noop` should not leave stale references
* `stop-except` & `start-without` to take all states ([#81](https://github.com/tolitius/mount/issues/81))
* goog.log/error for logging cljs errors ([#63](https://github.com/tolitius/mount/issues/63))
* on failure (down) reports and cleans up vs. just "throw" ([#63](https://github.com/tolitius/mount/issues/63))
* do not start a `DerefableState` as a side-effect of printing ([#76](https://github.com/tolitius/mount/issues/76))
## 0.1.11
###### Fri Dec 2 18:09:51 2016 -0600
* opening `(find-all-states)`
* stop accepts a collection of states (in addition to varargs) ([#69](https://github.com/tolitius/mount/issues/69))
* swap-states and start-with-states take values ([#68](https://github.com/tolitius/mount/issues/68))
* adding `(running-states)` that returns a set
* `(mount/start #{})` is a noop ([#65](https://github.com/tolitius/mount/issues/65))
* removing log on state discovery
* adding a restart listener with watchers
## 0.1.10
###### Sun Feb 28 18:20:52 2016 -0500
* runtime args are now initialized to `{}` (rather than to `:no-args`)
* composing states on mount start ([#47](https://github.com/tolitius/mount/issues/47))
* removing `:suspend` and `:resume` ([#46](https://github.com/tolitius/mount/issues/46))
## 0.1.9 ## 0.1.9
###### Sun Jan 31 15:47:19 2016 -0500
* `:on-reload` #{:noop :stop :restart} ([#36](https://github.com/tolitius/mount/issues/36)) * `:on-reload` #{:noop :stop :restart} ([#36](https://github.com/tolitius/mount/issues/36))
* swapping states with values ([#45](https://github.com/tolitius/mount/issues/45)) * swapping states with values ([#45](https://github.com/tolitius/mount/issues/45))

View file

@ -1,31 +0,0 @@
.PHONY: clean test jar tag outdated install deploy tree repl
clean:
rm -rf target
rm -rf classes
jar: clean test tag
clojure -A:jar
test: clean
clojure -X:test :patterns '[".*"]' # clojure tests
# clojure -Atest-cljs -r ".*test.self.host.*" # clojure script tests
# run "j8; boot test-cljs" until running cljs tests via deps.edn is fixed
outdated:
clojure -M:outdated
tag:
clojure -A:tag
install: jar
clojure -A:install
deploy: jar
clojure -A:deploy
tree:
mvn dependency:tree
repl:
clojure -A:dev -A:repl

331
README.md
View file

@ -1,13 +1,22 @@
> I think that it's _extraordinarily important_ that we in computer science keep fun in computing > I think that it's _extraordinarily important_ that we in computer science keep fun in computing
_**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](https://web.mit.edu/6.001/6.037/sicp.pdf)_ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](https://mitpress.mit.edu/sicp/full-text/book/book-Z-H-3.html)_
# mount <img src="doc/img/mount-logo.png" width="70px"> # mount
[![<! release](https://img.shields.io/badge/dynamic/json.svg?label=release&url=https%3A%2F%2Fclojars.org%2Fmount%2Flatest-version.json&query=version&colorB=blue)](https://github.com/tolitius/mount/releases)
[![<! clojars](https://img.shields.io/clojars/v/mount.svg)](https://clojars.org/mount)
###### _any_ questions or feedback: [`#mount`](https://clojurians.slack.com/messages/mount/) clojurians slack channel <img src="doc/img/slack-icon.png" width="15px"> (or just [open an issue](https://github.com/tolitius/mount/issues)) module | branch | status
----------|----------|----------
mount | `master` | [![Circle CI](https://circleci.com/gh/tolitius/mount/tree/master.png?style=svg)](https://circleci.com/gh/tolitius/mount/tree/master)
mount | `0.1.9-SNAPSHOT` | [![Circle CI](https://circleci.com/gh/tolitius/mount/tree/0.1.9-SNAPSHOT.png?style=svg)](https://circleci.com/gh/tolitius/mount/tree/0.1.9-SNAPSHOT)
[![Clojars Project](http://clojars.org/mount/latest-version.svg)](http://clojars.org/mount)
> <img src="doc/img/slack-icon.png" width="30px"> _any_ questions or feedback: [`#mount`](https://clojurians.slack.com/messages/mount/) clojurians slack channel (or just [open an issue](https://github.com/tolitius/mount/issues))
-
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Why?](#why) - [Why?](#why)
@ -20,25 +29,23 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Value of Values](#value-of-values) - [Value of Values](#value-of-values)
- [The Importance of Being Reloadable](#the-importance-of-being-reloadable) - [The Importance of Being Reloadable](#the-importance-of-being-reloadable)
- [Start and Stop Order](#start-and-stop-order) - [Start and Stop Order](#start-and-stop-order)
- [Composing States](#composing-states)
- [Start and Stop Parts of Application](#start-and-stop-parts-of-application) - [Start and Stop Parts of Application](#start-and-stop-parts-of-application)
- [Start an Application Without Certain States](#start-an-application-without-certain-states) - [Start an Application Without Certain States](#start-an-application-without-certain-states)
- [Stop an Application Except Certain States](#stop-an-application-except-certain-states)
- [Swapping Alternate Implementations](#swapping-alternate-implementations) - [Swapping Alternate Implementations](#swapping-alternate-implementations)
- [Swapping States with Values](#swapping-states-with-values) - [Swapping States with Values](#swapping-states-with-values)
- [Swapping States with States](#swapping-states-with-states) - [Swapping States with States](#swapping-states-with-states)
- [Stop an Application Except Certain States](#stop-an-application-except-certain-states) - [Suspending and Resuming](#suspending-and-resuming)
- [Suspendable Lifecycle](#suspendable-lifecycle)
- [Plugging into (reset)](#plugging-into-reset)
- [Suspendable Example Application](#suspendable-example-application)
- [ClojureScript is Clojure](doc/clojurescript.md#managing-state-in-clojurescript) - [ClojureScript is Clojure](doc/clojurescript.md#managing-state-in-clojurescript)
- [cljc mode](#cljc-mode)
- [Disable Lazy Start](#disable-lazy-start)
- [Packaging](#packaging) - [Packaging](#packaging)
- [Affected States](#affected-states) - [Affected States](#affected-states)
- [Recompiling Namespaces with Running States](#recompiling-namespaces-with-running-states) - [Recompiling Namespaces with Running States](#recompiling-namespaces-with-running-states)
- [:on-reload](#on-reload) - [:on-reload](#on-reload)
- [Cleaning up Deleted States](#cleaning-up-deleted-states) - [Cleaning up Deleted States](#cleaning-up-deleted-states)
- [Logging](#logging) - [Logging](#logging)
- [mount-up](#mount-up)
- [Manual AOP](#manual-aop)
- [Exception Handling](#exception-handling)
- [Clojure Version](#clojure-version) - [Clojure Version](#clojure-version)
- [Mount and Develop!](#mount-and-develop) - [Mount and Develop!](#mount-and-develop)
- [Running New York Stock Exchange](#running-new-york-stock-exchange) - [Running New York Stock Exchange](#running-new-york-stock-exchange)
@ -87,15 +94,15 @@ Pull request away, let's solve this thing!
Creating state is easy: Creating state is easy:
```clojure ```clojure
(defstate conn :start (create-conn)) (defstate conn :start create-conn)
``` ```
where the `create-conn` function creates a connection (for example to a database) and is defined elsewhere, can be right above it. where the `create-conn` function is defined elsewhere, can be right above it.
In case this state needs to be cleaned / destroyed between reloads, there is also `:stop` In case this state needs to be cleaned / destroyed between reloads, there is also `:stop`
```clojure ```clojure
(defstate conn :start (create-conn) (defstate conn :start create-conn
:stop (disconnect conn)) :stop (disconnect conn))
``` ```
@ -103,7 +110,7 @@ That is pretty much it. But wait, there is more.. this state is _a top level bei
`required` by other namespaces or in REPL: `required` by other namespaces or in REPL:
```clojure ```clojure
dev=> (require '[app.db :refer [conn]]) dev=> (require '[app.nyse :refer [conn]])
nil nil
dev=> conn dev=> conn
#object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"] #object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"]
@ -120,22 +127,6 @@ For example let's say an `app` needs a connection above. No problem:
where `above` is an arbitrary namespace that defines the above state / connection. where `above` is an arbitrary namespace that defines the above state / connection.
### Documentation String
As in any definition (i.e. `def`, `defn`) a documentation string can be added to better describe a state:
```clojure
(defstate answer
"answer to the ultimate question of life universe and everything"
:start (+ 1 41))
```
```clojure
(doc answer)
-------------------------
dev/answer
answer to the ultimate question of life universe and everything
```
## Dependencies ## Dependencies
If the whole app is one big application context (or `system`), cross dependencies with a solid dependency graph If the whole app is one big application context (or `system`), cross dependencies with a solid dependency graph
@ -172,13 +163,12 @@ this `config`, being top level, can be used in other namespaces, including the o
(defstate conn :start (create-connection config)) (defstate conn :start (create-connection config))
``` ```
[here](dev/clj/app/www.clj#L32) is an example of a web server that "depends" on a similar `config`. [here](dev/clj/app/www.clj#L30)
is an example of a web server that "depends" on a similar `config`.
###### _(the example `load-config` function above comes from [cprop](https://github.com/tolitius/cprop), but could of course be a custom function that loads configuration from a file)_
## Value of values ## Value of values
Lifecycle functions start/stop can take both functions and values. This is "valuable" and also works: Lifecycle functions start/stop/suspend/resume can take both functions and values. This is "valuable" and also works:
```clojure ```clojure
(defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42) (defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)
@ -269,75 +259,11 @@ dev=> (reset)
You can see examples of start and stop flows in the [example app](README.md#mount-and-develop). You can see examples of start and stop flows in the [example app](README.md#mount-and-develop).
## Composing States
Besides calling `(mount/start)` there are other useful ways to start an application:
* [starting parts of an application](README.md#start-and-stop-parts-of-application)
* [starting an application without certain states](README.md#start-an-application-without-certain-states)
* [swapping alternate implementations](README.md#swapping-alternate-implementations)
* [passing runtime arguments](README.md#runtime-arguments)
While all of these are great by themselves, sometimes it is really handy to compose these super powers. For example to start an application with _only_ certain states, _swapping_ a couple of them for new values, while passing runtime _arguments_.
### Composer's Toolbox
Each "tool" has a single responsibility and can be composed with other tools in _any_ combination and order.
* `only` will return _only_ states that it is given + exist (seen by mount) in the application
* `except` will return all the states that it is given _except_ a given set
* `swap` will take a map with keys as states and values as their substitute values
* `swap-states` will take a map with keys as states and values with `{:start fn :stop fn}` as their substitute states
* `with-args` will take a map that could later be accessed by `(mount/args)`
All these functions take one or two arguments. If called with two arguments, the first one will be treated as the universe of states to work with. If called with one argument, it will work with _all known_ to mount states.
None of these functions start or stop the application states, they merely serve as transformations from the initial set of states to the one that will later be passed to `(mount/start)`.
### Be Composing
All of the above is much easier to understand by looking at examples:
```clojure
(-> (only #{#'foo/a
#'foo/b
#'foo/c
#'bar/d
#'baz/e})
(except [#'foo/c
#'bar/d])
(with-args {:a 42})
mount/start)
```
This would start off from 5 states, even though the whole application may have many more states available. It would then exclude two states (i.e. `#'foo/c` and `#'bar/d`), then it will pass runtime arguments `{:a 42}`, and finally it will start the remaining three states: `#'foo/a`, `#'foo/b`, `#'baz/e`.
You may notice that `only` takes a set, while `except` takes a vector in this example. This is done intentionally to demonstrate that both these functions can take any collection of states. `set` would make more sense for most cases though.
Here is a more "involved" example:
```clojure
(-> (only #{#'foo/a
#'foo/b
#'foo/c
#'bar/d
#'baz/e})
(with-args {:a 42})
(except [#'foo/c
#'bar/d])
(swap-states {#'foo/a {:start #(create-connection test-conf)
:stop #(disconnect a)}})
(swap {#'baz/e {:datomic {:uri "datomic:mem://composable-mount"}}})
mount/start)
```
This will do the same thing as the previous example plus it would swap `#'foo/a` with alternative `:start` and `:stop` functions and `#'baz/e` with `{:datomic {:uri "datomic:mem://composable-mount"}}` value before starting the application.
## Start and Stop Parts of Application ## Start and Stop Parts of Application
In REPL or during testing it is often very useful to work with / start / stop _only a part_ of an application, i.e. "only these two states". In REPL or during testing it is often very useful to work with / start / stop _only a part_ of an application, i.e. "only these two states".
`mount`'s lifecycle functions, i.e. start/stop, can _optionally_ take states as vars (i.e. prefixed with their namespaces): `mount`'s lifecycle functions, i.e. start/stop/suspend/resume, can _optionally_ take states as vars (i.e. prefixed with their namespaces):
```clojure ```clojure
(mount/start #'app.config/config #'app.nyse/conn) (mount/start #'app.config/config #'app.nyse/conn)
@ -370,7 +296,7 @@ During testing it is often very useful to mock/stub certain states. For example
### Swapping States with Values ### Swapping States with Values
The `start-with` function takes values as substitutes. The `start-with` function takes values as substitues.
Say we have a `send-sms` state: Say we have a `send-sms` state:
@ -395,21 +321,14 @@ When running tests it would be great _not_ to send the real text messages, but r
### Swapping States with States ### Swapping States with States
The `start-with-states` function takes values in a form of `{:start fn :stop fn}` as substitutes: The `start-with-states` function takes other states as substitues:
```clojure ```clojure
(mount/start-with-states {#'app.neo/db {:start #(connect test-config) (mount/start-with {#'app.neo/db #'app.test/test-db
:stop #(disconnect db)} #'app.neo/publisher #'app.test/test-publisher})
#'app.neo/publisher {:start #(create-pub test-config)
:stop #(close-pub publisher)}})
``` ```
`start-with-states` takes a map of states with their substitutes. For example `#'app.nyse/db` here is the real deal (remote) DB that is being `start-with-states` takes a map of states with their substitutes. For example `#'app.nyse/db` here is the real deal (remote) DB that is being substituted with `#'app.test/test-db` state, which could be anything, a map, an in memory DB, etc.
substituted with `#(connect test-config)` function, which could end up being anything, a map, an in memory DB, etc.
The `:stop` functions of substitutes can be anything, and could refer to the original state references. As in the example above: `db` and `publisher`
are real references. They would need to be accessible from the namespace of course, so you might need to `(:require [app.neo :refer [db]])`
in order to use `db` in `:stop #(disconnect db)` example above.
-- --
@ -459,6 +378,80 @@ dev=> (mount/start)
Notice that the `nyse-app` is not started the second time (hence no more accidental `java.net.BindException: Address already in use`). It is already up and running. Notice that the `nyse-app` is not started the second time (hence no more accidental `java.net.BindException: Address already in use`). It is already up and running.
## Suspending and Resuming
Besides starting and stopping states can also be suspended and resumed. While this is not needed most of the time, it does comes really handy _when_ this need is there. For example:
* while working in REPL, you only want to truly restart a web server/queue listener/db connection _iff_ something changed, all other times `(mount/stop)` / `(mount/start)` or `(reset)` is called, these states should not be restarted. This might have to do with time to connect / bound ports / connection timeouts, etc..
* when taking an application out of rotation in a data center, and then phasing it back in, it might be handy to still keep it _up_, but suspend all the client / novelty facing components in between.
and some other use cases.
### Suspendable Lifecycle
In addition to `start` / `stop` functions, a state can also have `resume` and, if needed, `suspend` ones:
```clojure
(defstate web-server :start start-server
:resume resume-server
:stop stop-server)
```
`suspend` function is optional. Combining this with [(mount/stop-except)](#stop-an-application-except-certain-states), can result in an interesting restart behavior where everything is restared, but this `web-server` is _resumed_ instead (in this case `#'app.www/nyse-app` is an example of the above `web-server`):
```clojure
dev=> (mount/stop-except #'app.www/nyse-app)
14:44:33.991 [nREPL-worker-1] INFO mount.core - << stopping.. nrepl
14:44:33.992 [nREPL-worker-1] INFO mount.core - << stopping.. conn
14:44:33.992 [nREPL-worker-1] INFO app.db - disconnecting from datomic:mem://mount
14:44:33.992 [nREPL-worker-1] INFO mount.core - << stopping.. config
:stopped
dev=>
dev=> (mount/suspend)
14:44:52.467 [nREPL-worker-1] INFO mount.core - >> suspending.. nyse-app
:suspended
dev=>
dev=> (mount/start)
14:45:00.297 [nREPL-worker-1] INFO mount.core - >> starting.. config
14:45:00.297 [nREPL-worker-1] INFO mount.core - >> starting.. conn
14:45:00.298 [nREPL-worker-1] INFO app.db - creating a connection to datomic: datomic:mem://mount
14:45:00.315 [nREPL-worker-1] INFO mount.core - >> resuming.. nyse-app
14:45:00.316 [nREPL-worker-1] INFO mount.core - >> starting.. nrepl
:started
```
Notice `>> resuming.. nyse-app`, which in [this case](https://github.com/tolitius/mount/blob/suspendable/test/app/www.clj#L32) just recreates Datomic schema vs. doing that _and_ starting the actual web server.
### Plugging into (reset)
In case `tools.namespace` is used, this lifecycle can be easily hooked up with `dev.clj`:
```clojure
(defn start []
(mount/start))
(defn stop []
(mount/suspend)
(mount/stop-except #'app.www/nyse-app))
(defn reset []
(stop)
(tn/refresh :after 'dev/start))
```
### Suspendable Example Application
An [example application](https://github.com/tolitius/mount/tree/suspendable/test/app) with a suspendable web server and `dev.clj` lives in the `suspendable` branch. You can clone mount and try it out:
```
$ git checkout suspendable
Switched to branch 'suspendable'
```
## Recompiling Namespaces with Running States ## Recompiling Namespaces with Running States
Mount will detect when a namespace with states (i.e. with `(defstate ...)`) was reloaded/recompiled, Mount will detect when a namespace with states (i.e. with `(defstate ...)`) was reloaded/recompiled,
@ -480,9 +473,9 @@ Providing a `:stop` function _is_ optional, but in case a state needs to be clea
### :on-reload ### :on-reload
By default a state will be restarted on its redefinition or a namespace recompilation. However it is not always a desired behavior. Sometimes it's ok to have stale references during REPL sessions / development, other times all that is needed is not a "restart", but just a "stop". By default a state will be restarted on its redefenition or a namespace recompilation. However it is not always a desired behavior. Sometimes it's ok to have stale references during REPL sessions / development, other times all that is needed is not a "restart", but just a "stop".
This behavior could be controlled with an optional `:on-reload` meta attribute when defining a state. This behavior could be conrolled with an optional `:on-reload` meta attribute when defining a state.
In case _nothing_ needs to be done to a running state on reload / recompile / redef, set `:on-reload` to `:noop`: In case _nothing_ needs to be done to a running state on reload / recompile / redef, set `:on-reload` to `:noop`:
@ -502,8 +495,6 @@ When a running state needs to be just "stopped" on reload, set `:on-reload` to `
Again, by default, if no `:on-reload` meta is added, internally it would be set to `:restart`, in which case a running state will be restarted on a redef / a namespace reload. Again, by default, if no `:on-reload` meta is added, internally it would be set to `:restart`, in which case a running state will be restarted on a redef / a namespace reload.
Note that `^{:on-reload :noop}` will disable stopping or starting the state on namespace recompilation but it will still obey `(mount/start)` / `(mount/stop)` calls. This means that if any of the namespaces with `(mount/start)` / `(mount/stop)` calls are reloaded or these calls are explicitely executed (i.e. somewhere in the `dev` namespace or in an `:after` clause), the state's start/stop functions will still be called.
## Cleaning up Deleted States ## Cleaning up Deleted States
Mount will detect when a state was renamed/deleted from a namespace, and will do two things: Mount will detect when a state was renamed/deleted from a namespace, and will do two things:
@ -548,56 +539,6 @@ Mount detected that `#'dev/won't-be-here-long` was deleted, hence:
<< stopping.. #'dev/won't-be-here-long (it was deleted) << stopping.. #'dev/won't-be-here-long (it was deleted)
``` ```
## `cljc` mode
By default mount states are kept under var references. While it works for Clojure, it falls short in the land of ClojureScript since, especially during an `:advanced` compilation, var names get compressed + ClojureScript does not support reified vars.
To support both Clojure and ClojureScript mount has a `cljc` mode which is well documented in [here](doc/clojurescript.md#managing-state-in-clojurescript), and can be enabled by `(mount/in-cljc-mode)`.
### Disable Lazy Start
When in `cljc` mode, mount states that are not started by `(mount/start a b c)`, or that are not transitive states: i.e. not `:require`d at the time `(mount/start)` is called, will start lazily whenever they are dereferenced:
```clojure
=> (mount/in-cljc-mode)
:cljc
=> (defstate db-connection :start (println "connecting")
:stop (println "disconnecting..."))
=> db-connection
#object[mount.core.DerefableState 0x546b9d51 {:status :pending, :val nil}]
dev=> (mount/running-states)
#{}
dev=> @db-connection ;; db-connection will start here when deref'ed even though it was not started explicitly
connecting
dev=> (mount/running-states)
#{"#'dev/db-connection"}
```
This can be quite handy as it allows certain app states to start lazily.
However there are cases when it is best to fail in case a certain state is deref'ed while it was not yet started. This is possible by marking such states with `^{:on-lazy-start :throw}` metadata:
```clojure
=> (defstate ^{:on-lazy-start :throw} db-connection :start (do (println "connecting") 42)
:stop (println "disconnecting..."))
=> @db-connection ;; this will throw since db connection is deref'ed before it was started
java.lang.RuntimeException: :on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} #'dev/db-connection...) and #'dev/db-connection state was not explicitly started before it was deref'ed (i.e. @#'dev/db-connection)
=> (mount/start #'dev/db-connection)
connecting
{:started ["#'dev/db-connection"]}
=> @db-connection
42
```
## Packaging ## Packaging
Since `mount` relies on the Clojure/Script Compiler to learn about all the application states, before `mount/start` is called all the namespaces that have `defstate`s need to be compiled. Since `mount` relies on the Clojure/Script Compiler to learn about all the application states, before `mount/start` is called all the namespaces that have `defstate`s need to be compiled.
@ -625,22 +566,28 @@ In practice only a few namespaces need to be `:require`d, since others will be b
## Affected States ## Affected States
Every time a lifecycle function (start/stop) is called mount will return all the states that were affected: Every time a lifecycle function (start/stop/suspend/resume) is called mount will return all the states that were affected:
```clojure ```clojure
dev=> (mount/start) dev=> (mount/start)
{:started [#'app.config/config {:started [#'app.config/config
#'app.nyse/conn #'app.nyse/conn
#'app/nrepl]} #'app/nrepl
#'check.suspend-resume-test/web-server
#'check.suspend-resume-test/q-listener]}
``` ```
```clojure ```clojure
dev=> (mount/stop) dev=> (mount/suspend)
{:stopped [#'app/nrepl {:suspended [#'check.suspend-resume-test/web-server
#'app.nyse/conn #'check.suspend-resume-test/q-listener]}
#'app.config/config]} ```
```clojure
dev=> (mount/start)
{:started [#'check.suspend-resume-test/web-server
#'check.suspend-resume-test/q-listener]}
``` ```
An interesting bit here is a vector vs. a set: all the states are returned _in the order they were affected_. An interesting bit here is a vector vs. a set: all the states are returned _in the order they were changed_.
## Logging ## Logging
@ -650,39 +597,13 @@ Valid question. It was a [conscious choice](https://github.com/tolitius/mount/is
Since mount is a _library_ it should _not_ bring any dependencies unless its functionality directly depends on them. Since mount is a _library_ it should _not_ bring any dependencies unless its functionality directly depends on them.
> _But I still these logging statements in the examples..._ > But I still these logging statements in the examples.
### mount-up The way this is done is via an excellent [robert hooke](https://github.com/technomancy/robert-hooke/). Example applications live in `test`, so does the [utility](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/test/app/utils/logging.clj#L44) that adds logging to all the mount's lifecycle functions on start in [dev.clj](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/dev/dev.clj#L21).
One way to do that would be using "[mount-up](https://github.com/tolitius/mount-up)" that "watches mount's ups and downs":
```clojure
=> (require '[mount-up.core :as mu])
=> (mu/on-upndown :info mu/log :before)
=> (mount/start)
INFO mount-up.core - >> starting.. #'boot.user/server
{:started ["#'boot.user/server"]}
=> (mount/stop)
INFO mount-up.core - << stopping.. #'boot.user/server
{:stopped ["#'boot.user/server"]}
```
### Manual AOP
Another, a more manual way, would be to do it via an excellent [robert hooke](https://github.com/technomancy/robert-hooke/). Example applications live in `test`, so does the [utility](test/clj/tapp/utils/logging.clj#L42) that adds logging to all the mount's lifecycle functions on start in [dev.clj](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/dev/dev.clj#L21).
## Exception Handling
One way to handle exceptions on start/stop would be to simply wrap start/stop functions in `try/catch`.
Another way would be to use a custom [mount-up](https://github.com/tolitius/mount-up/blob/master/README.md#wrapping) wrapper.
## Clojure Version ## Clojure Version
Since mount [supports both](doc/clojurescript.md#managing-state-in-clojurescript) Clojure and ClojureScript, it relies on [Reader Conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) that were introduced in `Clojure 1.7`. mount's code is not precompiled (i.e. AOT) and distributed in `.cljc` sources, hence it currently requires `Clojure 1.7` and above. Since mount [supports both](doc/clojurescript.md#managing-state-in-clojurescript) Clojure and CljoureScript, it relies on [Reader Conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) that were introduced in `Clojure 1.7`. mount's code is not precompiled (i.e. AOT) and distributed in `.cljc` sources, hence it currently requires `Clojure 1.7` and above.
## Mount and Develop! ## Mount and Develop!
@ -802,7 +723,7 @@ dev=> (find-orders conn "TSLA")
### New York Stock Exchange Maintenance ### New York Stock Exchange Maintenance
Say we want to leave the exchange functioning, but would like to make sure that no one can hit it from the web. Easy, just stop the web server: Say we want to leave the exchange functioning, but would like to make sure that noone can hit it from the web. Easy, just stop the web server:
```clojure ```clojure
dev=> (mount/stop #'app.www/nyse-app) dev=> (mount/stop #'app.www/nyse-app)
@ -861,7 +782,7 @@ The documentation is [here](doc/runtime-arguments.md#passing-runtime-arguments).
## License ## License
Copyright © 2020 tolitius Copyright © 2015 tolitius
Distributed under the Eclipse Public License either version 1.0 or (at Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version. your option) any later version.

View file

@ -1,2 +0,0 @@
BOOT_VERSION=2.7.1
BOOT_CLOJURE_VERSION=1.10.1

View file

@ -1,56 +1,40 @@
(def +version+ "0.1.18") (def +version+ "0.1.9")
;; -- since boot is no more, this file will go away
;; -- once deps.edn "learns" how to run all cljs tests
;; j8; boot test
;; j8; boot test-cljs
;; j8; boot test-cljs-advanced
;; j8; boot -v build-jar push-snapshot
;; j8; boot -v build-jar push-release
(set-env! (set-env!
:source-paths #{"src"} :source-paths #{"src"}
:resource-paths #{"resources"} :dependencies '[;; mount brings _no dependencies_, everything here is for
:dependencies '[;; mount brings _NO DEPENDENCIES_, everything here is for
;; mount dev, examples apps and tests ;; mount dev, examples apps and tests
[org.clojure/clojure "1.8.0" :scope "provided"] [org.clojure/clojure "1.7.0" :scope "provided"]
[org.clojure/clojurescript "1.7.228" :scope "provided" :classifier "aot"] [org.clojure/clojurescript "1.7.189" :scope "provided" :classifier "aot"]
[datascript "0.15.0" :scope "provided"] [datascript "0.13.3" :scope "provided"]
[compojure "1.5.0" :scope "provided"] [compojure "1.4.0" :scope "provided"]
[ring/ring-jetty-adapter "1.1.0" :scope "provided"] [ring/ring-jetty-adapter "1.1.0" :scope "provided"]
[cheshire "5.5.0" :scope "provided"] [cheshire "5.5.0" :scope "provided"]
[hiccups "0.3.0" :scope "provided" :exclusions [org.clojure/clojurescript]] [hiccups "0.3.0" :scope "provided" :exclusions [org.clojure/clojurescript]]
[com.andrewmcveigh/cljs-time "0.3.14" :scope "provided"] [com.andrewmcveigh/cljs-time "0.3.14" :scope "provided"]
[tolitius/yang "0.1.9" :scope "provided"]
[ch.qos.logback/logback-classic "1.1.3" :scope "provided"] [ch.qos.logback/logback-classic "1.1.3" :scope "provided"]
[org.clojure/tools.logging "0.3.1" :scope "provided"] [org.clojure/tools.logging "0.3.1" :scope "provided"]
[robert/hooke "1.3.0" :scope "provided"] [robert/hooke "1.3.0" :scope "provided"]
[org.clojure/tools.namespace "0.2.11" :scope "provided"] [org.clojure/tools.namespace "0.2.11" :scope "provided"]
[org.clojure/tools.nrepl "0.2.12" :scope "provided"] [org.clojure/tools.nrepl "0.2.12" :scope "provided"]
[com.datomic/datomic-free "0.9.5359" :scope "provided" :exclusions [joda-time]] [com.datomic/datomic-free "0.9.5327" :scope "provided" :exclusions [joda-time]]
;; proto repl for fun and joy
[proto-repl "0.3.1" :scope "provided"]
[proto-repl-charts "0.3.2" :scope "provided"]
;; boot clj ;; boot clj
[boot/core "2.7.1" :scope "provided"] [boot/core "2.5.1" :scope "provided"]
[adzerk/bootlaces "0.1.13" :scope "test"] [adzerk/bootlaces "0.1.13" :scope "test"]
[adzerk/boot-logservice "1.2.0" :scope "test"] [adzerk/boot-logservice "1.0.1" :scope "test"]
[adzerk/boot-test "1.1.1" :scope "test"] [adzerk/boot-test "1.0.6" :scope "test"]
[tolitius/boot-check "0.1.2" :scope "test"] [tolitius/boot-check "0.1.1" :scope "test"]
;; boot cljs ;; boot cljs
[adzerk/boot-cljs "1.7.228-1" :scope "test"] [adzerk/boot-cljs "1.7.170-3" :scope "test"]
[adzerk/boot-cljs-repl "0.4.0" :scope "test"] [adzerk/boot-cljs-repl "0.3.0" :scope "test"]
[cider/piggieback "0.3.9" :scope "test" :exclusions [org.clojure/clojurescript]]
[weasel "0.7.0" :scope "test" :exclusions [org.clojure/clojurescript]]
[nrepl "0.4.5" :scope "test"]
[pandeiro/boot-http "0.7.1-SNAPSHOT" :scope "test"] [pandeiro/boot-http "0.7.1-SNAPSHOT" :scope "test"]
[tolitius/boot-stripper "0.1.0-SNAPSHOT" :scope "test"] [tolitius/boot-stripper "0.1.0-SNAPSHOT" :scope "test"]
[adzerk/boot-reload "0.4.8" :scope "test"] [com.cemerick/piggieback "0.2.1" :scope "test" :exclusions [org.clojure/clojurescript]]
[weasel "0.7.0" :scope "test" :exclusions [org.clojure/clojurescript]]
[adzerk/boot-reload "0.4.2" :scope "test"]
[crisptrutski/boot-cljs-test "0.2.1-SNAPSHOT" :scope "test"]]) [crisptrutski/boot-cljs-test "0.2.1-SNAPSHOT" :scope "test"]])
(require '[adzerk.bootlaces :refer :all] (require '[adzerk.bootlaces :refer :all]
@ -143,9 +127,7 @@
(cljs :optimizations :advanced :ids #{"mount"}))) (cljs :optimizations :advanced :ids #{"mount"})))
(task-options! (task-options!
tcs/test-cljs {:js-env :phantom} push {:ensure-branch nil}
push {; :ensure-clean nil
:ensure-branch nil}
pom {:project 'mount pom {:project 'mount
:version +version+ :version +version+
:description "managing Clojure and ClojureScript app state since (reset)" :description "managing Clojure and ClojureScript app state since (reset)"

15
circle.yml Normal file
View file

@ -0,0 +1,15 @@
machine:
java:
version: oraclejdk8
dependencies:
pre:
- wget https://github.com/boot-clj/boot-bin/releases/download/2.5.2/boot.sh
- mv boot.sh boot && chmod a+x boot && sudo mv boot /usr/local/bin
test:
override:
- boot test
- boot test-cljs
- boot test-cljs-advanced
- lein test2junit

View file

@ -1,52 +0,0 @@
{:paths ["src" "resources"]
:deps {} ;; deps no deps
:aliases {:dev {:extra-deps {metosin/jsonista {:mvn/version "0.3.8"}
com.datomic/datomic-free {:mvn/version "0.9.5359"
:exclusions [joda-time/joda-time]}
org.clojure/tools.nrepl {:mvn/version "0.2.12"}
org.clojure/tools.namespace {:mvn/version "0.2.11"}
cheshire/cheshire {:mvn/version "5.5.0"}
compojure/compojure {:mvn/version "1.5.0"}
ring/ring-jetty-adapter {:mvn/version "1.1.0"}
robert/hooke {:mvn/version "1.3.0"}
proto-repl/proto-repl {:mvn/version "0.3.1"}
proto-repl-charts/proto-repl-charts {:mvn/version "0.3.2"}
nrepl/nrepl {:mvn/version "0.7.0"}}}
:test {:extra-paths ["test/core" "test/clj" "test/cljs" "test/resources"]
:extra-deps {com.datomic/datomic-free {:mvn/version "0.9.5359"
:exclusions [joda-time/joda-time]}
org.clojure/tools.nrepl {:mvn/version "0.2.12"}
robert/hooke {:mvn/version "1.3.0"}
org.clojure/tools.logging {:mvn/version "1.3.0"}
io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "e7660458ce25bc4acb4ccc3e2415aae0a4907198"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}
:test-cljs {:extra-paths ["test/core" "test/cljs" "test/resources"]
:extra-deps {org.clojure/clojure {:mvn/version "1.8.0"}
org.clojure/clojurescript {:mvn/version "1.7.228"}
com.andrewmcveigh/cljs-time {:mvn/version "0.3.14"}
hiccups/hiccups {:mvn/version "0.3.0"}
datascript/datascript {:mvn/version "0.15.0"}
olical/cljs-test-runner {:mvn/version "3.8.1"}}
:main-opts ["-m" "cljs-test-runner.main"]}
:repl {:extra-paths ["dev/clj"]
:extra-deps {cider/cider-nrepl {:mvn/version "0.22.4"}
org.clojure/tools.logging {:mvn/version "1.2.4"}
com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
:main-opts ["-e" "(require 'dev)(in-ns 'dev)"
"-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"
"-i" "-f" "rebel-readline.main/-main"]}
:outdated {:extra-deps {olical/depot {:mvn/version "2.0.1"}}
:main-opts ["-m" "depot.outdated.main" "-a" "outdated"]}
:tag {:extra-deps {tolitius/tag {:mvn/version "0.1.7"}}
:main-opts ["-m" "tag.core" "tolitius/mount" "managing Clojure and ClojureScript app state since (reset)"]}
:jar {:extra-deps {seancorfield/depstar {:mvn/version "1.1.128"}}
:extra-paths ["target/about"]
:main-opts ["-m" "hf.depstar.jar" "target/mount.jar" "--exclude" "clojure/core/specs/alpha.*"]}
:deploy {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}}
:main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "target/mount.jar"]}
:install {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}}
:main-opts ["-m" "deps-deploy.deps-deploy" "install" "target/mount.jar"]}}}

View file

@ -6,38 +6,41 @@
(alter-meta! *ns* assoc ::load false) (alter-meta! *ns* assoc ::load false)
(defn- f-to-action [f {:keys [status]}] (defn- f-to-action [f]
(let [fname (-> (str f) (let [fname (-> (str f)
(split #"@") (split #"@")
first)] first)]
(case fname (case fname
"mount.core$up" (when-not (:started status) :up) "mount.core$up" :up
"mount.core$down" (when-not (:stopped status) :down) "mount.core$down" :down
"mount.core$sigstop" :suspend
"mount.core$sigcont" :resume
:noop))) :noop)))
(defn whatcha-doing? [action] (defn whatcha-doing? [{:keys [status suspend]} action]
(case action (case action
:up ">> starting" :up (if (status :suspended) ">> resuming"
:down "<< stopping" (if-not (status :started) ">> starting"))
false)) :down (if (or (status :started) (status :suspended)) "<< stopping")
:suspend (if (and (status :started) suspend) "<< suspending")
:resume (if (status :suspended) ">> resuming")))
(defn log-status [f & args] (defn log-status [f & args]
(let [[state-name state] args (let [{:keys [var] :as state} (second args)
action (f-to-action f state)] action (f-to-action f)]
(when-let [taking-over-the-world (whatcha-doing? action)] (when-let [taking-over-the-world (whatcha-doing? state action)]
(info (str taking-over-the-world ".. " state-name))) (info (str taking-over-the-world ".. " var)))
(apply f args))) (apply f args)))
(defonce lifecycle-fns (defonce lifecycle-fns
#{#'mount.core/up #{#'mount.core/up
#'mount.core/down}) #'mount.core/down
#'mount.core/sigstop
#'mount.core/sigcont})
(defn without-logging-status [] (defn without-logging-status []
(doall (map #(clear-hooks %) lifecycle-fns))) (doall (map #(clear-hooks %) lifecycle-fns)))
;; this is the one to use:
(defn with-logging-status [] (defn with-logging-status []
(without-logging-status) (without-logging-status)
(doall (map #(add-hook % log-status) lifecycle-fns))) (doall (map #(add-hook % log-status) lifecycle-fns)))

View file

@ -1,6 +1,7 @@
(ns dev (ns dev
(:require [clojure.pprint :refer [pprint]] (:require [clojure.pprint :refer [pprint]]
[clojure.tools.namespace.repl :as tn] [clojure.tools.namespace.repl :as tn]
[boot.core :refer [load-data-readers!]]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[mount.tools.graph :refer [states-with-deps]] [mount.tools.graph :refer [states-with-deps]]
[app.utils.logging :refer [with-logging-status]] [app.utils.logging :refer [with-logging-status]]
@ -40,11 +41,4 @@
(tn/refresh :after 'dev/go)) (tn/refresh :after 'dev/go))
(mount/in-clj-mode) (mount/in-clj-mode)
(defn load-data-readers!
"Refresh *data-readers* with readers from newly acquired dependencies."
[]
(#'clojure.core/load-data-readers)
(set! *data-readers* (.getRawRoot #'*data-readers*)))
(load-data-readers!) (load-data-readers!)

View file

@ -1,15 +0,0 @@
(ns proto-play
(:require [mount.tools.graph :as mg]
[proto-repl-charts.graph :as proto]))
(defn mount->proto [graph]
(reduce (fn [g {:keys [name deps]}]
(-> g
(update :nodes conj name)
(update :edges conj (-> deps (conj name) vec))))
{}
graph))
(->> (mg/states-with-deps)
mount->proto
(proto/graph "a proto graph of mount states"))

View file

@ -42,7 +42,7 @@ Mount has two modes `clj` and `cljc`.
#### Clojure _and_ ClojureScript Mode #### Clojure _and_ ClojureScript Mode
`cljc` is not a default mode, but it is easy to switch to: `cljc` mode is is not default, but it is easy to switch to:
To switch Mount into this mode do: To switch Mount into this mode do:
@ -83,16 +83,14 @@ Let's look at the example [ClojureScript app](../dev/cljs/app) that uses mount t
In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with: In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with:
``` ```clojure
$ boot cljs-example $ lein do clean, cljsbuild once prod
Started Jetty on http://localhost:3000
nREPL server started on port 64412 on host 127.0.0.1 - nrepl://127.0.0.1:64412
Adding :require adzerk.boot-cljs-repl to mount.cljs.edn...
Compiling ClojureScript... Compiling ClojureScript...
• mount.js Compiling "dev/resources/public/js/compiled/mount.js" from ["src" "dev/cljs"]...
Successfully compiled "dev/resources/public/js/compiled/mount.js" in 23.966 seconds.
``` ```
And just open a browser at [http://localhost:3000](http://localhost:3000): And just open a browser at `file:///[path-to-mount]/mount/dev/resources/public/index.html`:
<img src="img/mount.cljs.example.png" width="700"> <img src="img/mount.cljs.example.png" width="700">

View file

@ -31,7 +31,9 @@ The not so hidden benefit is REPL time reloadability that it brings to the table
- [Boilerplate code](#boilerplate-code) - [Boilerplate code](#boilerplate-code)
- [Library vs. Framework](#library-vs-framework) - [Library vs. Framework](#library-vs-framework)
- [What Component does better](#what-component-does-better) - [What Component does better](#what-component-does-better)
- [Multiple separate systems within the same JVM](#multiple-separate-systems-within-the-same-jvm) - [Swapping alternate implementations](#swapping-alternate-implementations)
- [Uberjar / Packaging](#uberjar--packaging)
- [Multiple separate systems](#multiple-separate-systems)
- [Visualizing dependency graph](#visualizing-dependency-graph) - [Visualizing dependency graph](#visualizing-dependency-graph)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -55,7 +57,7 @@ Before moving on to differences, [here](https://news.ycombinator.com/item?id=246
Component really only works if you build your entire app around its model: application is fully based on Components Component really only works if you build your entire app around its model: application is fully based on Components
where every Component is an Object. where every Component is an Object.
Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/wherever Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/whereever
you need it and use it. you need it and use it.
This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "_open_" application This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "_open_" application
@ -94,7 +96,7 @@ the same with or without Mount: there are no extra mental steps.
### Objects vs. Namespaces ### Objects vs. Namespaces
One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything. One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything.
This is how Component "separates explicit dependencies" and "clears the boundaries". This is how Component "separates explicit dependencies" and "clears the bounaries".
This is also how an Object Oriented language does it, which does not leave a lot of room for functions: This is also how an Object Oriented language does it, which does not leave a lot of room for functions:
with Component most of the functions are _methods_ which is an important distinction. with Component most of the functions are _methods_ which is an important distinction.
@ -146,23 +148,48 @@ no "ceremony".
Mount uses namespaces and vars where Component uses records and protocols. Mount uses namespaces and vars where Component uses records and protocols.
Component manages records and protocols, Component manages protocols and records, and in order to do that it requires a whole app buyin, which makes it a _framework_.
and in order to do that it requires a whole app buy-in,
which makes it a _framework_.
Mount does not need to manage namespaces and vars, Mount does not need to manage namespaces and vars, since it is very well managed by the Clojure Compiler, which makes it a _library_.
since it is very well managed by the Clojure Compiler,
which makes it a _library_.
## What Component does better ## What Component does better
### Multiple separate systems within the same JVM ### Swapping alternate implementations
With Component multiple separate systems can be started _in the same Clojure runtime_ with different settings. Which _might_ be useful for testing, i.e. if you need to have `dev db` and `test db` started in the _same_ REPL, to _run tests within the same REPL you develop in_. This is someting that is very useful for testing and is very easy to do in Component by simply assoc'ing onto a map.
Development workflows vary and tend to be a subjective / preference based more than a true recipe, but I believe it is much cleaner to run tests in the _separate_ REPL / process. Moreover run them continuously: i.e. `boot watch speak test`: this way you don't event need to look at that other REPL / terminal, Boot will _tell_ you whether the tests pass or fail after any file is changed. Mount can do it too: https://github.com/tolitius/mount#swapping-alternate-implementations
Mount keeps states in namespaces, hence the app becomes "[The One](https://en.wikipedia.org/wiki/Neo_(The_Matrix))", and there can't be "multiples The Ones". In practice, if we are talking about stateful external resources, there is trully only _one_ of them with a given configuration. Different configuration => different state. It's that simple. The reason it is in "Component does it better" section is because, while result is the same, merging maps is a bit simpler than:
```clojure
(mount/start-with {#'app.nyse/db #'app.test/test-db
#'app.nyse/publisher #'app.test/test-publisher})
```
### Uberjar / Packaging
Since Component fully controls the `system` where the whole application lives, it is quite simple
to start an application from anywhere including a `-main` function of the uberjar.
In order to start the whole system in development, Mount just needs `(mount/start)` or `(reset)`
it's [simple](https://github.com/tolitius/mount#the-importance-of-being-reloadable).
However there is no "tools.namespaces"/REPL at a "stand alone jar runtime" and in order for Mount to start / stop
the app, states need to be `:require`/`:use`d, which is usually done within the same namespace as `-main`.
Depending on app dependencies, it could only require a few states to be `:require`/`:use`d, others
will be brought transitively. Here is more about mount [packaging](https://github.com/tolitius/mount#packaging) and an [example](uberjar.md#creating-reloadable-uberjarable-app) of building a wepapp uberjar with Mount.
On the flip side, Component _system_ usually requires all the `:require`s, since in order to be built, it needs to "see" all the top level states.
###### _conclusion: it's simple in Mount as well, but is left upto developer to require what's needed._
### Multiple separate systems
With Component multiple separate systems can be started _in the same Clojure runtime_ with different settings. Which might be useful for testing.
Mount keeps states in namespaces, hence the app becomes "[The One](https://en.wikipedia.org/wiki/Neo_(The_Matrix))", and there can't be "multiples The Ones".
Testing is not alien to Mount and it knows how to do a thing or two: Testing is not alien to Mount and it knows how to do a thing or two:
@ -170,13 +197,13 @@ Testing is not alien to Mount and it knows how to do a thing or two:
* [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states) * [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states)
* [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations) * [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations)
* [stop an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states) * [stop an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states)
* [composing states](https://github.com/tolitius/mount#composing-states) * [suspending and resuming](https://github.com/tolitius/mount#suspending-and-resuming)
After [booting mount](http://www.dotkam.com/2015/12/22/the-story-of-booting-mount/) I was secretly thinking of achieving multiple separate systems by running them in different [Boot Pods](https://github.com/boot-clj/boot/wiki/Pods). But running two apps in the same JVM side by side with "same but different" states, is not something Mount can do at the moment.
But the more I think about it, the less it feels like a mount's core functionality. So I created [Yurt](https://github.com/tolitius/yurt) that can easily create and run multiple separate mount systems simultaniously. After [booting mount](http://www.dotkam.com/2015/12/22/the-story-of-booting-mount/) I am secretly thinking of achieving multiple separate systems by running them in different [Boot Pods](https://github.com/boot-clj/boot/wiki/Pods), but for now it remains to be a secret hypothesis.
###### _conclusion: can be done with mount as well, but via a different dependency._ ###### _conclusion: needs more thinking._
### Visualizing dependency graph ### Visualizing dependency graph

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 215 KiB

View file

@ -1,23 +0,0 @@
{
"name": "@tolitius/mount",
"version": "0.1.16",
"license": "EPL-1.0",
"homepage": "https://github.com/tolitius/mount",
"repository": {
"type": "git",
"url": "https://github.com/tolitius/mount"
},
"author": {
"name": "tolitius",
"url": "http://www.dotkam.com"
},
"files": [
"src/*"
],
"directories": {
"lib": "src"
},
"dependencies": {
"ws": "^8.16.0"
}
}

45
pom.xml
View file

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>mount</groupId>
<artifactId>mount</artifactId>
<version>0.1.23</version>
<name>mount</name>
<description>managing Clojure and ClojureScript app state since (reset)</description>
<url>https://github.com/tolitius/mount</url>
<licenses>
<license>
<name>Eclipse Public License</name>
<url>http://www.eclipse.org/legal/epl-v10.html</url>
</license>
</licenses>
<developers>
<developer>
<name>tolitius</name>
</developer>
</developers>
<scm>
<url>https://github.com/tolitius/mount</url>
<connection>scm:git:git://github.com/tolitius/mount.git</connection>
<developerConnection>scm:git:ssh://git@github.com/tolitius/mount.git</developerConnection>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.11.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
</build>
<repositories>
<repository>
<id>clojars</id>
<url>https://repo.clojars.org/</url>
</repository>
</repositories>
</project>

View file

@ -1,4 +1,4 @@
(defproject mount "0.1.17" (defproject mount "0.1.9-SNAPSHOT"
:description "managing Clojure and ClojureScript app state since (reset)" :description "managing Clojure and ClojureScript app state since (reset)"
:url "https://github.com/tolitius/mount" :url "https://github.com/tolitius/mount"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"
@ -8,12 +8,9 @@
:dependencies [] ;; for visual clarity :dependencies [] ;; for visual clarity
:tach {:test-runner-ns 'mount.test-self-host
:source-paths ["test/core"]}
:profiles {:dev {:source-paths ["dev" "dev/clj" "test/clj"] :profiles {:dev {:source-paths ["dev" "dev/clj" "test/clj"]
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.9.946"]; :classifier "aot"] [org.clojure/clojurescript "1.7.170"]; :classifier "aot"]
[datascript "0.13.3"] [datascript "0.13.3"]
[compojure "1.4.0"] [compojure "1.4.0"]
[ring/ring-jetty-adapter "1.1.0"] [ring/ring-jetty-adapter "1.1.0"]
@ -30,8 +27,7 @@
:plugins [[lein-cljsbuild "1.1.1"] :plugins [[lein-cljsbuild "1.1.1"]
[lein-doo "0.1.6"] [lein-doo "0.1.6"]
[lein-figwheel "0.5.0-2"] [lein-figwheel "0.5.0-2"]
[test2junit "1.1.3"] [test2junit "1.1.3"]]
[lein-tach "1.0.0"]]
:test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit") :test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit")

View file

@ -1,3 +0,0 @@
{:linters {:mount/defstate {:level :warning}}
:hooks {:analyze-call {mount.core/defstate hooks.defstate/defstate
mount.core/defstate! hooks.defstate/defstate}}}

View file

@ -1,39 +0,0 @@
(ns hooks.defstate
(:require [clj-kondo.hooks-api :as api]))
(defn defstate [{:keys [node]}]
(let [[n & args] (next (:children node))
[docs args] (if (string? (api/sexpr (first args)))
[(first args) (next args)]
[nil args])
m (when-let [m (first (:meta n))]
(api/sexpr m))
m (if (map? m) m {})
ks (cond-> (take 1 args)
(> (count args) 2) (conj (nth args 2)))
invalid-key (first (remove (comp (partial contains? #{:start :stop}) api/sexpr) ks))]
(cond
invalid-key
(api/reg-finding!
{:message (str "lifecycle functions can only contain `:start` and `:stop`. illegal function found: " (api/sexpr invalid-key))
:type :mount/defstate
:row (:row (meta invalid-key))
:col (:col (meta invalid-key))})
(not (contains? (set (map api/sexpr ks)) :start))
(throw (ex-info "lifecycle functions must include `:start`" {}))
((complement contains?) #{2 4} (count args))
(throw (ex-info "lifecycle functions must consist of no more than 2 pair forms: `:start` and `:stop`" {}))
(and (contains? m :on-reload) (not (contains? #{:noop :stop} (:on-reload m))))
(api/reg-finding!
{:message "metadata `:on-reload` key can only have value of `noop` or `stop`"
:type :mount/defstate
:row (:row (meta n))
:col (:col (meta n))})
:else
{:node (api/list-node
(cond-> [(api/token-node 'def) n]
docs (conj docs)
true (conj (api/list-node
(list*
(api/token-node 'do)
args)))))})))

View file

@ -1,23 +1,22 @@
(ns mount.core (ns mount.core
#?(:clj {:clojure.tools.namespace.repl/load false}) ; prevent reloading of this ns
#?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro] #?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro]
[mount.tools.macrovich :refer [deftime]]
[mount.tools.logger :refer [log]] [mount.tools.logger :refer [log]]
[clojure.set :refer [intersection]]
[clojure.string :as s]) [clojure.string :as s])
:cljs (:require [mount.tools.macro] :cljs (:require [mount.tools.macro :as macro]
[clojure.set :refer [intersection]]
[mount.tools.logger :refer [log]])) [mount.tools.logger :refer [log]]))
#?(:cljs (:require-macros [mount.core] #?(:cljs (:require-macros [mount.core]
[mount.tools.macro :refer [on-error throw-runtime]] [mount.tools.macro :refer [if-clj on-error throw-runtime]])))
[mount.tools.macrovich :refer [deftime]])))
(defonce ^:private -args (atom {})) ;; mostly for command line args and external files (defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
(defonce ^:private state-seq (atom 0)) (defonce ^:private state-seq (atom 0))
(defonce ^:private mode (atom :clj)) (defonce ^:private mode (atom :clj))
(defonce ^:private meta-state (atom {})) (defonce ^:private meta-state (atom {}))
(defonce ^:private running (atom {})) ;; to clean dirty states on redefs (defonce ^:private running (atom {})) ;; to clean dirty states on redefs
;; supporting tools.namespace: (disable-reload!)
#?(:clj
(alter-meta! *ns* assoc ::load false)) ;; to exclude the dependency
(defn- make-state-seq [state] (defn- make-state-seq [state]
(or (:order (@meta-state state)) (or (:order (@meta-state state))
(swap! state-seq inc))) (swap! state-seq inc)))
@ -31,7 +30,8 @@
(defn- validate [{:keys [start stop suspend resume] :as lifecycle}] (defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
(cond (cond
(not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)") (not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)")
(or suspend resume) (throw-runtime "suspend / resume lifecycle support was removed in \"0.1.10\" in favor of (mount/stop-except)"))) (and suspend
(not resume)) (throw-runtime "suspendable state should have a resume function (i.e. missing :resume fn)")))
(defn- with-ns [ns name] (defn- with-ns [ns name]
(str "#'" ns "/" name)) (str "#'" ns "/" name))
@ -57,6 +57,17 @@
(stop)) (stop))
(swap! running dissoc state))) (swap! running dissoc state)))
#?(:clj
(defn current-state [state]
(let [{:keys [inst var]} (@meta-state state)]
(if (= @mode :cljc)
@inst
(var-get var))))
:cljs
(defn current-state [state]
(-> (@meta-state state) :inst deref)))
#?(:clj #?(:clj
(defn alter-state! [{:keys [var inst]} value] (defn alter-state! [{:keys [var inst]} value]
(if (= @mode :cljc) (if (= @mode :cljc)
@ -75,36 +86,39 @@
(swap! done conj state-name) (swap! done conj state-name)
state)) state))
(defn- up [state {:keys [start stop status] :as current} done] (defn- up [state {:keys [start stop resume status] :as current} done]
(when-not (:started status) (when-not (:started status)
(let [s (on-error (str "could not start [" state "] due to") (let [s (on-error (str "could not start [" state "] due to")
(record! state start done))] (if (:suspended status)
(record! state resume done)
(record! state start done)))]
(alter-state! current s) (alter-state! current s)
(swap! running assoc state {:stop stop}) (swap! running assoc state {:stop stop})
(update-meta! [state :status] #{:started})))) (update-meta! [state :status] #{:started}))))
(defn- down (defn- down [state {:keys [stop status] :as current} done]
"brings a state down by (when (some status #{:started :suspended})
* calling its 'stop' function if it is defined (when stop
* if not defined, state will still become a 'NotStartedState' (on-error (str "could not stop [" state "] due to")
* in case of a failure on 'stop', state is still marked as :stopped, and the error is logged / printed (record! state stop done)))
* dissoc'ing it from the running states (alter-state! current (NotStartedState. state)) ;; (!) if a state does not have :stop when _should_ this might leak
* marking it as :stopped"
[state {:keys [stop status] :as current} done]
(when (some status #{:started})
(if stop
(if-let [cause (-> (on-error (str "could not stop [" state "] due to")
(record! state stop done)
:fail? false)
:f-failed)]
(log cause :error) ;; this would mostly be useful in REPL / browser console
(alter-state! current (->NotStartedState state)))
(alter-state! current (->NotStartedState state))) ;; (!) if a state does not have :stop when _should_ this might leak
(swap! running dissoc state) (swap! running dissoc state)
(update-meta! [state :status] #{:stopped}))) (update-meta! [state :status] #{:stopped})))
(defn running-states [] (defn- sigstop [state {:keys [resume suspend status] :as current} done]
(set (keys @running))) (when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
(when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
(let [s (on-error (str "could not suspend [" state "] due to")
(record! state suspend done))]
(alter-state! current s)))
(update-meta! [state :status] #{:suspended})))
(defn- sigcont [state {:keys [resume status] :as current} done]
(when (:suspended status)
(let [s (on-error (str "could not resume [" state "] due to")
(record! state resume done))]
(alter-state! current s)
(update-meta! [state :status] #{:started}))))
(deftype DerefableState [name] (deftype DerefableState [name]
#?(:clj clojure.lang.IDeref #?(:clj clojure.lang.IDeref
@ -112,46 +126,19 @@
(#?(:clj deref (#?(:clj deref
:cljs -deref) :cljs -deref)
[_] [_]
(let [{:keys [status var inst] :as state} (@meta-state name)] (let [{:keys [status inst] :as state} (@meta-state name)]
(when-not (:started status) (when-not (:started status)
(if (= :throw (-> var meta :on-lazy-start)) (up name state (atom #{})))
(throw-runtime (str ":on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} " name "...) " @inst)))
"and " name " state was not explicitly started before it was deref'ed (i.e. @" name ")"))
(up name state (atom #{}))))
@inst))
#?(:clj clojure.lang.IPending
:cljs IPending)
(#?(:clj isRealized
:cljs -realized?)
[_]
(boolean ((running-states) name))))
#?(:clj
(defn current-state [state]
(let [{:keys [var]} (@meta-state state)]
(if (= @mode :cljc)
(->DerefableState state)
(var-get var))))
:cljs
(defn current-state [state]
(-> (@meta-state state) :inst deref)))
(defn on-reload-meta [s-var] (defn on-reload-meta [s-var]
(or (-> s-var meta :on-reload) (or (-> s-var meta :on-reload)
:restart)) ;; restart by default on ns reload :restart)) ;; restart by default on ns reload
(defn running-noop? [s-name]
(let [{:keys [var status]} (@meta-state s-name)
on-reload (-> var meta :on-reload)]
(when status
(and (status :started)
(= :noop on-reload)))))
;;TODO: make private after figuring out the inconsistency betwen cljs compile stages ;;TODO: make private after figuring out the inconsistency betwen cljs compile stages
;; (i.e. _sometimes_ this, if private, is not seen by expanded "defmacro" on cljs side) ;; (i.e. _sometimes_ this, if private, is not seen by expanded "defmacro" on cljs side)
(defn mount-it [s-var s-name s-meta] (defn mount-it [s-var s-name s-meta]
(let [with-inst (assoc s-meta :inst (atom (->NotStartedState s-name)) (let [with-inst (assoc s-meta :inst (atom (NotStartedState. s-name))
:var s-var) :var s-var)
on-reload (on-reload-meta s-var) on-reload (on-reload-meta s-var)
existing? (when-not (= :noop on-reload) existing? (when-not (= :noop on-reload)
@ -161,42 +148,32 @@
(log (str ">> starting.. " s-name " (namespace was recompiled)")) (log (str ">> starting.. " s-name " (namespace was recompiled)"))
(up s-name with-inst (atom #{}))))) (up s-name with-inst (atom #{})))))
(deftime #?(:clj
(defmacro defstate [state & body]
(defmacro defstate (let [[state params] (macro/name-with-attributes state body)
"defines a state (a.k.a. a stateful component). {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)
restarts on recompilation.
pass ^{:on-reload :noop} to prevent auto-restart on ns recompilation,
or ^{:on-reload :stop} to stop on recompilation."
[state & body]
(let [[state params] (mount.tools.macro/name-with-attributes state body)
{:keys [start stop] :as lifecycle} (apply hash-map params)
state-name (with-ns *ns* state) state-name (with-ns *ns* state)
order (make-state-seq state-name)] order (make-state-seq state-name)]
(validate lifecycle) (validate lifecycle)
(let [s-meta (cond-> {:order order (let [s-meta (cond-> {:order order
:start `(fn [] ~start) :start `(fn [] ~start)
:status #{:stopped}} :status #{:stopped}}
stop (assoc :stop `(fn [] ~stop)))] stop (assoc :stop `(fn [] ~stop))
suspend (assoc :suspend `(fn [] ~suspend))
resume (assoc :resume `(fn [] ~resume)))]
`(do `(do
;; (log (str "|| mounting... " ~state-name)) (~'defonce ~state (DerefableState. ~state-name))
;; only create/redefine a new state iff this is not a running ^{:on-reload :noop} (mount-it (~'var ~state) ~state-name ~s-meta)
(if-not (running-noop? ~state-name) (~'var ~state))))))
(do
(~'defonce ~state (->DerefableState ~state-name))
(mount-it (~'var ~state) ~state-name ~s-meta))
(~'defonce ~state (current-state ~state-name)))
(~'var ~state)))))
(defmacro defstate! [state & {:keys [start! stop!]}] #?(:clj
(defmacro defstate! [state & {:keys [start! stop!]}]
(let [state-name (with-ns *ns* state)] (let [state-name (with-ns *ns* state)]
`(defstate ~state `(defstate ~state
:start (~'let [~state (mount.core/current-state ~state-name)] :start (~'let [~state (mount/current-state ~state-name)]
~start!) ~start!)
:stop (~'let [~state (mount.core/current-state ~state-name)] :stop (~'let [~state (mount/current-state ~state-name)]
~stop!)))) ~stop!)))))
)
(defn in-cljc-mode [] (defn in-cljc-mode []
(reset! mode :cljc)) (reset! mode :cljc))
@ -207,7 +184,7 @@
;;TODO args might need more thinking ;;TODO args might need more thinking
(defn args [] @-args) (defn args [] @-args)
(defn find-all-states [] (defn- find-all-states []
(keys @meta-state)) (keys @meta-state))
#?(:clj #?(:clj
@ -249,14 +226,14 @@
(defn- merge-lifecycles (defn- merge-lifecycles
"merges with overriding _certain_ non existing keys. "merges with overriding _certain_ non existing keys.
i.e. :stop is in a 'state', but not in a 'substitute': it should be overriden with nil i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil
however other keys of 'state' (such as :ns,:name,:order) should not be overriden" however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
([state sub] ([state sub]
(merge-lifecycles state nil sub)) (merge-lifecycles state nil sub))
([state origin {:keys [start stop status]}] ([state origin {:keys [start stop suspend resume status]}]
(assoc state :origin origin (assoc state :origin origin
:status status :status status
:start start :stop stop))) :start start :stop stop :suspend suspend :resume resume)))
(defn- rollback! [state] (defn- rollback! [state]
(let [{:keys [origin] :as sub} (@meta-state state)] (let [{:keys [origin] :as sub} (@meta-state state)]
@ -264,11 +241,13 @@
(update-meta! [state] (merge-lifecycles sub origin))))) (update-meta! [state] (merge-lifecycles sub origin)))))
(defn- substitute! [state with mode] (defn- substitute! [state with mode]
(let [lifecycle-fns #(select-keys % [:start :stop :status]) (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
origin (@meta-state state) origin (@meta-state state)
sub (if (= :value mode) sub (if (= :value mode)
{:start (fn [] with) :status :stopped} {:start (fn [] with) :status :stopped}
(assoc with :status :stopped))] (@meta-state with))]
(when (= :state mode)
(update-meta! [with :sub?] true))
(update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub)))) (update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub))))
(defn- unsub [state] (defn- unsub [state]
@ -279,108 +258,21 @@
(remove (comp :sub? @meta-state) (find-all-states))) (remove (comp :sub? @meta-state) (find-all-states)))
(defn start [& states] (defn start [& states]
(let [fs (-> states first)] (let [states (or (seq states) (all-without-subs))]
(if (coll? fs) {:started (bring states up <)}))
(if-not (empty? fs) ;; (mount/start) vs. (mount/start #{}) vs. (mount/start #{1 2 3})
(apply start fs)
{:started #{}})
(let [states (or (->> states (map var-to-str) seq)
(all-without-subs))]
{:started (bring states up <)}))))
(defn stop [& states] (defn stop [& states]
(let [fs (-> states first)] (let [states (or states (find-all-states))
(if (coll? fs) _ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
(if-not (empty? fs) ;; (mount/stop) vs. (mount/stop #{}) vs. (mount/stop #{1 2 3})
(apply stop fs)
{:stopped #{}})
(let [states (or (->> states (map var-to-str) seq)
(find-all-states))
_ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with" / "swap-states"
stopped (bring states down >)] stopped (bring states down >)]
(dorun (map rollback! states)) ;; restore to origin from "start-with" / "swap-states" (dorun (map rollback! states)) ;; restore to origin from "start-with"
{:stopped stopped})))) {:stopped stopped}))
;; composable set of states
(defn- mapset [f xs]
(-> (map f xs)
set))
(defn only
([these]
(only (find-all-states) these))
([states these]
(intersection (mapset var-to-str these)
(mapset var-to-str states))))
(defn with-args
([args]
(with-args (find-all-states) args))
([states args]
(reset! -args args) ;; TODO localize
states))
(defn except
([these]
(except (find-all-states) these))
([states these]
(remove (mapset var-to-str these)
(mapset var-to-str states))))
(defn swap
([with]
(swap (find-all-states) with))
([states with]
(doseq [[from to] with]
(substitute! (var-to-str from)
to :value))
states))
(defn swap-states
([with]
(swap-states (find-all-states) with))
([states with]
(doseq [[from to] with]
(substitute! (var-to-str from)
to :state))
states))
;; restart on events
(defprotocol ChangeListener
(add-watcher [this ks watcher])
(on-change [this k]))
(deftype RestartListener [watchers]
ChangeListener
(add-watcher [_ ks state]
(doseq [k ks]
(swap! watchers update k (fn [v]
(-> (conj v state) vec)))))
(on-change [_ ks]
(doseq [k ks]
(when-let [states (seq (@watchers k))]
(apply stop states)
(apply start states)))))
(defn restart-listener
([]
(restart-listener {}))
([watchers]
(RestartListener. (atom watchers))))
;; explicit, not composable (subject to depreciate?)
(defn stop-except [& states] (defn stop-except [& states]
(let [all (set (find-all-states)) (let [all (set (find-all-states))
states (map var-to-str states) states (map var-to-str states)
states (remove (set states) all)] states (remove (set states) all)]
(if-not (empty? states) (apply stop states)))
(apply stop states)
{:stopped #{}})))
(defn start-with-args [xs & states] (defn start-with-args [xs & states]
(reset! -args xs) (reset! -args xs)
@ -397,7 +289,7 @@
(defn start-with-states [with] (defn start-with-states [with]
(doseq [[from to] with] (doseq [[from to] with]
(substitute! (var-to-str from) (substitute! (var-to-str from)
to :state)) (var-to-str to) :state))
(start)) (start))
(defn start-without [& states] (defn start-without [& states]
@ -405,7 +297,13 @@
(let [app (set (all-without-subs)) (let [app (set (all-without-subs))
states (map var-to-str states) states (map var-to-str states)
without (remove (set states) app)] without (remove (set states) app)]
(if-not (empty? without) (apply start without))
(apply start without)
{:started #{}}))
(start))) (start)))
(defn suspend [& states]
(let [states (or (seq states) (all-without-subs))]
{:suspended (bring states sigstop <)}))
(defn resume [& states]
(let [states (or (seq states) (all-without-subs))]
{:resumed (bring states sigcont <)}))

View file

@ -24,3 +24,4 @@
meta-with-ns) meta-with-ns)
states) states)
(sort-by :order))))) (sort-by :order)))))

View file

@ -3,18 +3,16 @@
(:import [goog.debug Console])])) (:import [goog.debug Console])]))
#?(:cljs #?(:cljs
(defonce ^:dynamic *logger* (defonce *logger*
(do (do
(.setCapturing (Console.) true) (.setCapturing (Console.) true)
(glog/getLogger "mount" nil)))) (glog/getLogger "mount"))))
#?(:clj #?(:clj
(defn log [msg & _] (defn log [msg]
(prn msg))) (prn msg)))
#?(:cljs #?(:cljs
(defn log [msg & level] (defn log [msg]
(case (first level) (glog/info *logger* msg)))
:error (glog/error *logger* msg nil)
(glog/info *logger* msg nil))))

View file

@ -1,28 +1,26 @@
(ns mount.tools.macro (ns mount.tools.macro
(:refer-clojure :exclude [case]) #?(:cljs (:require-macros [mount.tools.macro])))
#?(:cljs (:require-macros [mount.tools.macrovich :refer [deftime]])
:clj (:require [mount.tools.macrovich :refer [deftime]])))
(deftime #?(:clj
(defmacro if-clj [then else]
(if (-> &env :ns not)
then
else)))
(defmacro on-error [msg f & {:keys [fail?] #?(:clj
:or {fail? true}}] (defmacro on-error [msg f]
`(mount.tools.macrovich/case `(if-clj
:clj (try ~f (try ~f
(catch Throwable t# (catch Throwable t#
(if ~fail? (throw (RuntimeException. ~msg t#))))
(throw (RuntimeException. ~msg t#)) (try ~f
{:f-failed (ex-info ~msg {} t#)})))
:cljs (try ~f
(catch :default t# (catch :default t#
(if ~fail? (throw (~'str ~msg " " t#)))))))
(throw (js/Error (~'str ~msg " " t#)))
{:f-failed (ex-info ~msg {} t#)})))))
#?(:clj
(defmacro throw-runtime [msg] (defmacro throw-runtime [msg]
`(throw (mount.tools.macrovich/case :clj (RuntimeException. ~msg) :cljs (~'str ~msg)))) `(throw (if-clj (RuntimeException. ~msg)
(~'str ~msg)))))
)
;; this is a one to one copy from https://github.com/clojure/tools.macro ;; this is a one to one copy from https://github.com/clojure/tools.macro
;; to avoid a lib dependency for a single function ;; to avoid a lib dependency for a single function

View file

@ -1,21 +0,0 @@
(ns ^{:doc "From https://github.com/cgrand/macrovich. Licensed under EPL v1.
Copyright Cristophe Grand." }
mount.tools.macrovich
(:refer-clojure :exclude [case]))
(defmacro deftime
"This block will only be evaluated at the correct time for macro definition, at other times its content
are removed.
For Clojure it always behaves like a `do` block.
For Clojurescript/JVM the block is only visible to Clojure.
For self-hosted Clojurescript the block is only visible when defining macros in the pseudo-namespace."
[& body]
(when #?(:clj (not (:ns &env)) :cljs (re-matches #".*\$macros" (name (ns-name *ns*))))
`(do ~@body)))
(defmacro case [& {:keys [cljs clj]}]
(if (contains? &env '&env)
`(if (:ns ~'&env) ~cljs ~clj)
(if #?(:clj (:ns &env) :cljs true)
cljs
clj)))

View file

@ -6,9 +6,8 @@
(defn entity [conn id] (defn entity [conn id]
(d/entity (d/db conn) id)) (d/entity (d/db conn) id))
(defn touch (defn touch [conn results]
"takes 'entity ids' results from a query "takes 'entity ids' results from a query
e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'" e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'"
[conn results]
(let [e (partial entity conn)] (let [e (partial entity conn)]
(map #(-> % first e d/touch) results))) (map #(-> % first e d/touch) results)))

View file

@ -6,37 +6,40 @@
(alter-meta! *ns* assoc ::load false) (alter-meta! *ns* assoc ::load false)
(defn- f-to-action [f {:keys [status]}] (defn- f-to-action [f]
(let [fname (-> (str f) (let [fname (-> (str f)
(split #"@") (split #"@")
first)] first)]
(case fname (case fname
"mount.core$up" (when-not (:started status) :up) "mount.core$up" :up
"mount.core$down" (when-not (:stopped status) :down) "mount.core$down" :down
"mount.core$sigstop" :suspend
"mount.core$sigcont" :resume
:noop))) :noop)))
(defn whatcha-doing? [action] (defn whatcha-doing? [{:keys [status suspend]} action]
(case action (case action
:up ">> starting" :up (if (status :suspended) ">> resuming"
:down "<< stopping" (if-not (status :started) ">> starting"))
false)) :down (if (or (status :started) (status :suspended)) "<< stopping")
:suspend (if (and (status :started) suspend) "<< suspending")
:resume (if (status :suspended) ">> resuming")))
(defn log-status [f & args] (defn log-status [f & args]
(let [[state-name state] args (let [{:keys [var] :as state} (second args)
action (f-to-action f state)] action (f-to-action f)]
(when-let [taking-over-the-world (whatcha-doing? action)] (when-let [taking-over-the-world (whatcha-doing? state action)]
(info (str taking-over-the-world ".. " state-name))) (info (str taking-over-the-world ".. " var)))
(apply f args))) (apply f args)))
(defonce lifecycle-fns (defonce lifecycle-fns
#{#'mount.core/up #{#'mount.core/up
#'mount.core/down}) #'mount.core/down
#'mount.core/sigstop
#'mount.core/sigcont})
(defn without-logging-status [] (defn without-logging-status []
(doall (map #(clear-hooks %) lifecycle-fns))) (doall (map clear-hooks lifecycle-fns)))
;; this is the one to use:
(defn with-logging-status [] (defn with-logging-status []
(without-logging-status) (without-logging-status)

View file

@ -13,7 +13,7 @@
mount.test.start-without mount.test.start-without
mount.test.start-with mount.test.start-with
mount.test.start-with-states mount.test.start-with-states
mount.test.printing mount.test.suspend-resume
)) ))
#?(:clj (alter-meta! *ns* assoc ::load false)) #?(:clj (alter-meta! *ns* assoc ::load false))
@ -32,7 +32,7 @@
'mount.test.start-without 'mount.test.start-without
'mount.test.start-with 'mount.test.start-with
'mount.test.start-with-states 'mount.test.start-with-states
'mount.test.printing 'mount.test.suspend-resume
)) ))
(defn run-tests [] (defn run-tests []

View file

@ -1,190 +0,0 @@
(ns mount.test.composable-fns
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[clojure.set :refer [intersection]]
[mount.core :refer [only except swap swap-states with-args] :as mount :refer-macros [defstate]]
[tapp.websockets :refer [system-a]]
[tapp.conf :refer [config]]
[tapp.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[clojure.set :refer [intersection]]
[clojure.tools.nrepl.server :refer [start-server stop-server]]
[mount.core :as mount :refer [defstate only except swap swap-states with-args]]
[tapp.conf :refer [config]]
[tapp.nyse :refer [conn]]
[tapp.example :refer [nrepl]]])
[mount.test.helper :refer [dval helper]]))
#?(:clj (alter-meta! *ns* assoc ::load false))
(defstate test-conn :start 42
:stop (constantly 0))
(defstate test-nrepl :start [])
(def swap-conn {:start (fn [] 42)
:stop #(println "stopping test-conn-state")})
#?(:clj
(def swap-nrepl {:start #(start-server :bind "localhost" :port 3442)
:stop #(stop-server @nrepl)}))
#?(:clj
(deftest only-states
(testing "only should only return given states.
if source set of states is not provided, it should use all the states to select from"
(is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl" "#'tapp.nyse/conn"}
(only #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}))))
(testing "only should only return given states"
(is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl"}
(only [#'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn]
#{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl}))))))
#?(:clj
(deftest except-states
(testing "except should exclude given states.
if source set of states is not provided, it should use all the states to exclude from"
(let [states (except #{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn})]
(is (coll? states))
(is (pos? (count states)))
(is (zero? (count (intersection (set states)
#{"#'tapp.example/nrepl" "#'tapp.nyse/conn" "#'is.not/here"}))))))
(testing "except should exclude given states"
(is (= #{"#'tapp.conf/config" "#'mount.test.composable-fns/test-conn"}
(set (except #{#'tapp.example/nrepl #'tapp.conf/config #'mount.test.composable-fns/test-conn}
#{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn})))))))
#?(:clj
(deftest states-with-args
(testing "with-args should set args and return all states if none provided"
(let [states (with-args {:a 42})]
(is (= {:a 42} (mount/args)))
(is (= states (#'mount.core/find-all-states)))))
(testing "with-args should set args and thread states if provided"
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}
states (with-args t-states {:a 42})]
(is (= {:a 42} (mount/args)))
(is (= states t-states))))))
#?(:clj
(deftest swap-states-with-values
(testing "swap should swap states with values and return all states if none is given"
(let [states (swap {#'tapp.nyse/conn "conn-sub"
#'tapp.example/nrepl :nrepl-sub})]
(is (= states (#'mount.core/find-all-states)))
(mount/start)
(is (map? (dval config)))
(is (= :nrepl-sub (dval nrepl)))
(is (= "conn-sub" (dval conn)))
(mount/stop)))
(testing "swap should swap states with values and return only states that it is given"
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}
states (swap t-states {#'tapp.nyse/conn "conn-sub"
#'tapp.example/nrepl :nrepl-sub})]
(is (= states t-states))
(mount/start)
(is (map? (dval config)))
(is (= :nrepl-sub (dval nrepl)))
(is (= "conn-sub" (dval conn)))
(is (= 42 (dval test-conn)))
(mount/stop)))))
#?(:clj
(deftest swap-states-with-states
(testing "swap-states should swap states with states and return all mount states if none is given"
(let [states (swap-states {#'tapp.nyse/conn swap-conn
#'tapp.example/nrepl swap-nrepl})]
(is (= states (#'mount.core/find-all-states)))
(mount/start)
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (= 42 (dval conn)))
(mount/stop)))
(testing "swap-states should swap states on start and rollback on stop"
(let [states (swap-states {#'tapp.nyse/conn swap-conn})]
(is (= states (#'mount.core/find-all-states)))
(mount/start #'tapp.nyse/conn)
(is (= 42 (dval conn)))
(mount/stop #'tapp.nyse/conn)
(mount/start #'tapp.nyse/conn)
(is (instance? datomic.peer.LocalConnection (dval conn)))
(mount/stop)))
(testing "swap-states should swap states with states and return only states that it is given"
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.nyse/conn}
states (swap-states t-states {#'tapp.nyse/conn swap-conn
#'tapp.example/nrepl swap-nrepl})]
(is (= states t-states))
(apply mount/start states)
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (= 42 (dval conn)))
(is (= 42 (dval test-conn))) ;; test-conn is explicitly started via "t-states"
(mount/stop)))))
#?(:clj
(deftest composing
(testing "states provided to the top level should narrow down the scope for the whole composition"
(let [scope [#'tapp.conf/config
#'tapp.example/nrepl
#'tapp.nyse/conn
#'mount.test.composable-fns/test-nrepl
#'mount.test.composable-fns/test-conn]
states (-> (only scope)
(with-args {:a 42})
(except [#'mount.test.composable-fns/test-nrepl
#'mount.test.composable-fns/test-conn])
(swap-states {#'tapp.example/nrepl swap-nrepl})
(swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}}))]
(is (= #{"#'tapp.nyse/conn" "#'tapp.conf/config" "#'tapp.example/nrepl"} (set states)))
(mount/start states)
(is (= {:a 42} (mount/args)))
(is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(mount/stop)))
(testing "should compose and start in a single composition"
(let [scope [#'tapp.conf/config
#'tapp.example/nrepl
#'tapp.nyse/conn
#'mount.test.composable-fns/test-nrepl
#'mount.test.composable-fns/test-conn]]
(-> (only scope)
(with-args {:a 42})
(except [#'mount.test.composable-fns/test-nrepl
#'mount.test.composable-fns/test-conn])
(swap-states {#'tapp.example/nrepl swap-nrepl})
(swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}})
mount/start)
(is (= {:a 42} (mount/args)))
(is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(mount/stop)))
(testing "should not start anything on empty seq of states"
(let [scope #{}]
(is (= {:started #{}} (-> (only scope)
mount/start)))
(mount/stop)))
(testing "should not stop anything on empty seq of states"
(let [scope #{}]
(mount/start)
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= {:stopped #{}} (-> (only scope)
mount/stop)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(mount/stop)
(is (instance? mount.core.NotStartedState (dval conn)))))))

View file

@ -35,9 +35,8 @@
(require 'mount.test.on-reload-helper :reload) (require 'mount.test.on-reload-helper :reload)
;; "a" is marked as :noop on reload ;; "a" is marked as :noop on reload
;; previous behavior left a stale reference =>>> ;; (is (instance? mount.core.NotStartedState (dval a))) ;; (!) stale reference of old a is still there somewhere (is (instance? mount.core.NotStartedState (dval a))) ;; (!) stale reference of old a is still there somewhere
(is (= :started (dval a))) ;; make sure a still has the same instance as before reload (is (= (-> pre-reload :a)
(is (= (-> pre-reload :a) ;; and the start was not called: the counter did not change
(-> @counter :a))) (-> @counter :a)))
;; "b" is marked as :stop on reload ;; "b" is marked as :stop on reload

View file

@ -1,17 +0,0 @@
(ns mount.test.printing
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]])))
#?(:clj (alter-meta! *ns* assoc ::load false))
(defstate foo
:start (do (println "Starting!") 42))
(deftest test-printing-has-no-side-effects
;; Test that printing an unstarted DerefableState does not have the
;; side-effect of starting it
(println foo)
(is (not= 42 foo)))

View file

@ -7,7 +7,6 @@
[tapp.audit-log :refer [log]]] [tapp.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[clojure.tools.nrepl.server :refer [start-server stop-server]]
[tapp.conf :refer [config]] [tapp.conf :refer [config]]
[tapp.nyse :refer [conn]] [tapp.nyse :refer [conn]]
[tapp.example :refer [nrepl]]]) [tapp.example :refer [nrepl]]])
@ -20,29 +19,20 @@
(defstate test-nrepl :start []) (defstate test-nrepl :start [])
(def swap-conn {:start (fn [] 42)
:stop #(println "stopping test-conn-state")})
#?(:clj
(def swap-nrepl {:start #(start-server :bind "localhost" :port 3442)
:stop #(stop-server @nrepl)})
:cljs
(def swap-nrepl {:start (fn [] :nrepl)
:stop (fn [] :stopped-nrepl)}))
#?(:cljs #?(:cljs
(deftest start-with-states (deftest start-with-states
(testing "should start with substitutes" (testing "should start with substitutes"
(let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn (let [_ (mount/start-with-states {#'tapp.websockets/system-a #'mount.test.start-with-states/test-conn
#'mount.test.helper/helper swap-nrepl})] #'mount.test.helper/helper #'mount.test.start-with-states/test-nrepl})]
(is (map? (dval config))) (is (map? (dval config)))
(is (= (:nrepl (dval helper)))) (is (vector? (dval helper)))
(is (= (dval system-a) 42)) (is (= (dval system-a) 42))
(is (instance? datascript.db/DB @(dval log))) (is (instance? datascript.db/DB @(dval log)))
(mount/stop))) (mount/stop)))
#_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states (testing "should not start the substitute itself"
(let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn})] (let [_ (mount/start-with-states {#'tapp.websockets/system-a #'mount.test.start-with-states/test-conn})]
(is (instance? mount.core.NotStartedState (dval test-conn))) (is (instance? mount.core.NotStartedState (dval test-conn)))
(is (= 42 (dval system-a))) (is (= 42 (dval system-a)))
(mount/stop))) (mount/stop)))
@ -72,15 +62,15 @@
(deftest start-with-states (deftest start-with-states
(testing "should start with substitutes" (testing "should start with substitutes"
(let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn (let [_ (mount/start-with-states {#'tapp.nyse/conn #'mount.test.start-with-states/test-conn
#'tapp.example/nrepl swap-nrepl})] #'tapp.example/nrepl #'mount.test.start-with-states/test-nrepl})]
(is (map? (dval config))) (is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) (is (vector? (dval nrepl)))
(is (= (dval conn) 42)) (is (= (dval conn) 42))
(mount/stop))) (mount/stop)))
#_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states (testing "should not start the substitute itself"
(let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn})] (let [_ (mount/start-with-states {#'tapp.nyse/conn #'mount.test.start-with-states/test-conn})]
(is (instance? mount.core.NotStartedState (dval test-conn))) (is (instance? mount.core.NotStartedState (dval test-conn)))
(is (= (dval conn) 42)) (is (= (dval conn) 42))
(mount/stop))) (mount/stop)))

View file

@ -0,0 +1,212 @@
(ns mount.test.suspend-resume
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[tapp.websockets :refer [system-a]]
[tapp.conf :refer [config]]
[tapp.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[tapp.conf :refer [config]]
[tapp.nyse :refer [conn]]
[tapp.example :refer [nrepl]]])
[mount.test.helper :refer [dval]]))
#?(:clj (alter-meta! *ns* assoc ::load false))
(defn koncat [k s]
(-> (name k)
(str "-" (name s))
keyword))
(defn start [s] (koncat s :started))
(defn stop [s] (koncat s :stopped))
(defn suspend [s] (koncat s :suspended))
(defn resume [s] (koncat s :resumed))
(defstate web-server :start (start :w)
:stop (stop :w)
:suspend (suspend :w)
:resume (resume :w))
(defstate q-listener :start (start :q)
:stop (stop :q)
:suspend (suspend :q)
:resume (resume :q))
(defstate randomizer :start (rand-int 42))
#?(:cljs
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? (dval config)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? js/WebSocket (dval system-a)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'tapp.websockets/system-a)
_ (mount/suspend)
_ (mount/resume)]
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval system-a)))
(is (instance? datascript.db/DB @(dval log)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? (dval config)))
(is (instance? js/WebSocket (dval system-a)))
(is (instance? datascript.db/DB @(dval log)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval system-a)))
(is (instance? mount.core.NotStartedState (dval log)))
(is (instance? mount.core.NotStartedState (dval web-server)))))))
#?(:cljs
(deftest suspendable-start-with-states
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with-states {#'tapp.websockets/system-a #'mount.test.suspend-resume/web-server})
_ (mount/suspend)]
(is (= (dval system-a) :w-suspended))
(is (instance? mount.core.NotStartedState (dval web-server)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? js/WebSocket (dval system-a)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))))
#?(:clj
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'tapp.example/nrepl)
_ (mount/suspend)
_ (mount/resume)]
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (instance? mount.core.NotStartedState (dval conn)))
(is (instance? mount.core.NotStartedState (dval web-server)))))))
#?(:clj
(deftest suspendable-start-with-states
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with-states {#'tapp.example/nrepl #'mount.test.suspend-resume/web-server})
_ (mount/suspend)]
(is (= (dval nrepl) :w-suspended))
(is (instance? mount.core.NotStartedState (dval web-server)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
;; (unchanged/not substituted in its lexical scope), and original state won't be started
(testing "when replacing a suspendable state with a non suspendable one,
the later should not be suspendable,
the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start-with-states {#'mount.test.suspend-resume/web-server #'mount.test.suspend-resume/randomizer})
_ (mount/suspend)]
(is (integer? (dval web-server)))
(is (instance? mount.core.NotStartedState (dval randomizer)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (integer? (dval randomizer)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable started one,
the later should not be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with-states {#'mount.test.suspend-resume/web-server #'tapp.nyse/conn}) ;; TODO: good to WARN on started states during "start-with-states"
_ (mount/suspend)]
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a suspendable one,
the later should be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with-states {#'mount.test.suspend-resume/web-server
#'mount.test.suspend-resume/q-listener})] ;; TODO: good to WARN on started states during "start-with-states"
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :q-resumed))
(mount/suspend)
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :q-suspended))
(mount/stop)
(is (instance? mount.core.NotStartedState (dval web-server)))
(is (instance? mount.core.NotStartedState (dval q-listener)))
(mount/start)
(mount/suspend)
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :w-suspended))
(mount/stop)))))

View file

@ -1,29 +0,0 @@
(ns mount.test-self-host
(:require
[cljs.test :as t]
mount.test.fun-with-values
mount.test.private-fun
mount.test.printing
mount.test.parts
mount.test.cleanup-dirty-states
mount.test.stop-except
mount.test.start-without
mount.test.start-with
mount.test.start-with-states
))
(t/run-tests
'mount.test.fun-with-values
'mount.test.private-fun
'mount.test.printing
'mount.test.parts
'mount.test.cleanup-dirty-states
;; 'mount.test.stop-except ;; TODO: can't run with deps.edn (due to "WebSocket is not defined")
;; 'mount.test.start-with ;; boot, lein have no problems
;; 'mount.test.start-with-states ;; most likely somm misconfigured in node..
'mount.test.start-without
)
(defn run-tests []
(t/run-all-tests #"mount.test.*"))