├── dev ├── resources │ ├── mount.cljs.edn │ ├── public │ │ └── index.html │ └── config.edn ├── user.clj ├── cljs │ └── app │ │ ├── conf.cljs │ │ ├── audit_log.cljs │ │ ├── websockets.cljs │ │ └── example.cljs └── clj │ ├── app │ ├── conf.clj │ ├── utils │ │ ├── datomic.clj │ │ └── logging.clj │ ├── nyse.clj │ ├── example.clj │ ├── www.clj │ └── db.clj │ ├── proto_play.clj │ └── dev.clj ├── test ├── resources │ └── mount.cljs.edn ├── cljs │ └── tapp │ │ ├── conf.cljs │ │ ├── audit_log.cljs │ │ ├── websockets.cljs │ │ └── example.cljs ├── clj │ └── tapp │ │ ├── conf.clj │ │ ├── utils │ │ ├── datomic.clj │ │ └── logging.clj │ │ ├── nyse.clj │ │ └── example.clj └── core │ └── mount │ ├── test │ ├── var │ │ ├── private_fun.clj │ │ └── fun_with_values.clj │ ├── printing.cljc │ ├── private_fun.cljc │ ├── helper.cljc │ ├── on_reload_helper.cljc │ ├── parts.cljc │ ├── start_without.cljc │ ├── cleanup_deleted_states.cljc │ ├── fun_with_values.cljc │ ├── on_reload.cljc │ ├── cleanup_dirty_states.cljc │ ├── stop_except.cljc │ ├── start_with.cljc │ ├── start_with_states.cljc │ └── composable_fns.cljc │ ├── test_self_host.cljs │ └── test.cljc ├── boot.properties ├── .clj-kondo └── config.edn ├── doc ├── img │ ├── get-uberjar.png │ ├── mount-logo.png │ ├── ns-recompile.png │ ├── post-uberjar.png │ ├── slack-icon.png │ ├── cljs-ns-reload.png │ ├── welcome-uberjar.png │ └── mount.cljs.example.png ├── runtime-arguments.md ├── clojurescript.md ├── uberjar.md └── differences-from-component.md ├── .hgignore ├── resources └── clj-kondo.exports │ └── mount │ └── mount │ ├── config.edn │ └── hooks │ └── defstate.clj ├── .gitignore ├── package.json ├── src └── mount │ ├── tools │ ├── logger.cljc │ ├── macrovich.cljc │ ├── graph.cljc │ └── macro.cljc │ └── core.cljc ├── .travis.yml ├── Makefile ├── pom.xml ├── project.clj ├── deps.edn ├── CHANGELOG.md ├── LICENSE └── README.md /dev/resources/mount.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [app.example]} 2 | -------------------------------------------------------------------------------- /test/resources/mount.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [mount.test]} 2 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_VERSION=2.7.1 2 | BOOT_CLOJURE_VERSION=1.10.1 3 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (defn dev [] 2 | (require 'dev) 3 | (in-ns 'dev)) 4 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../resources/clj-kondo.exports/mount/mount/"]} 2 | -------------------------------------------------------------------------------- /doc/img/get-uberjar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/get-uberjar.png -------------------------------------------------------------------------------- /doc/img/mount-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/mount-logo.png -------------------------------------------------------------------------------- /doc/img/ns-recompile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/ns-recompile.png -------------------------------------------------------------------------------- /doc/img/post-uberjar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/post-uberjar.png -------------------------------------------------------------------------------- /doc/img/slack-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/slack-icon.png -------------------------------------------------------------------------------- /doc/img/cljs-ns-reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/cljs-ns-reload.png -------------------------------------------------------------------------------- /doc/img/welcome-uberjar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/welcome-uberjar.png -------------------------------------------------------------------------------- /doc/img/mount.cljs.example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolitius/mount/HEAD/doc/img/mount.cljs.example.png -------------------------------------------------------------------------------- /dev/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | target/** 3 | classes/** 4 | checkouts/** 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .gitignore 12 | .git/** 13 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/mount/mount/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:mount/defstate {:level :warning}} 2 | :hooks {:analyze-call {mount.core/defstate hooks.defstate/defstate 3 | mount.core/defstate! hooks.defstate/defstate}}} 4 | -------------------------------------------------------------------------------- /dev/cljs/app/conf.cljs: -------------------------------------------------------------------------------- 1 | (ns app.conf 2 | (:require [app.audit-log :refer [audit log]]) 3 | (:require-macros [mount.core :refer [defstate]])) 4 | 5 | (defn load-config [path] 6 | (audit log :app-conf "loading config from '" path "' (at least pretending)") 7 | {:system-a {:uri "ws://echo.websocket.org/"}}) 8 | 9 | (defstate config :start (load-config "resources/config.end")) 10 | -------------------------------------------------------------------------------- /test/cljs/tapp/conf.cljs: -------------------------------------------------------------------------------- 1 | (ns tapp.conf 2 | (:require [tapp.audit-log :refer [audit log]]) 3 | (:require-macros [mount.core :refer [defstate]])) 4 | 5 | (defn load-config [path] 6 | (audit log :app-conf "loading config from '" path "' (at least pretending)") 7 | {:system-a {:uri "ws://echo.websocket.org/"}}) 8 | 9 | (defstate config :start (load-config "resources/config.end")) 10 | -------------------------------------------------------------------------------- /dev/clj/app/conf.clj: -------------------------------------------------------------------------------- 1 | (ns app.conf 2 | (:require [mount.core :as mount :refer [defstate]] 3 | [clojure.edn :as edn] 4 | [clojure.tools.logging :refer [info]])) 5 | 6 | (defn load-config [path] 7 | (info "loading config from" path) 8 | (-> path 9 | slurp 10 | edn/read-string)) 11 | 12 | (defstate config 13 | :start (load-config "dev/resources/config.edn")) 14 | -------------------------------------------------------------------------------- /dev/clj/app/utils/datomic.clj: -------------------------------------------------------------------------------- 1 | (ns app.utils.datomic 2 | (:require [datomic.api :as d])) 3 | 4 | (defn entity [conn id] 5 | (d/entity (d/db conn) id)) 6 | 7 | (defn touch [conn results] 8 | "takes 'entity ids' results from a query 9 | e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'" 10 | (let [e (partial entity conn)] 11 | (map #(-> % first e d/touch) results))) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | .cpcache/ 5 | .rebel_readline_history 6 | cljs-test-runner-out 7 | node_modules 8 | pom.xml.asc 9 | .repl* 10 | dev/resources/public/js/* 11 | figwheel_server.log 12 | build.xml 13 | doo-index.html 14 | *.jar 15 | *.class 16 | /.lein-* 17 | /.nrepl-port 18 | *.iml 19 | /.idea 20 | /.lein-repl-history 21 | /.nrepl-history 22 | .cljs_rhino_repl/ 23 | out/ 24 | .clj-kondo/.cache 25 | -------------------------------------------------------------------------------- /test/clj/tapp/conf.clj: -------------------------------------------------------------------------------- 1 | (ns tapp.conf 2 | (:require [mount.core :as mount :refer [defstate]] 3 | [clojure.edn :as edn] 4 | [clojure.tools.logging :refer [info]])) 5 | 6 | (alter-meta! *ns* assoc ::load false) 7 | 8 | (defn load-config [path] 9 | (info "loading config from" path) 10 | (-> path 11 | slurp 12 | edn/read-string)) 13 | 14 | (defstate config 15 | :start (load-config "dev/resources/config.edn")) 16 | -------------------------------------------------------------------------------- /test/clj/tapp/utils/datomic.clj: -------------------------------------------------------------------------------- 1 | (ns tapp.utils.datomic 2 | (:require [datomic.api :as d])) 3 | 4 | (alter-meta! *ns* assoc ::load false) 5 | 6 | (defn entity [conn id] 7 | (d/entity (d/db conn) id)) 8 | 9 | (defn touch 10 | "takes 'entity ids' results from a query 11 | e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'" 12 | [conn results] 13 | (let [e (partial entity conn)] 14 | (map #(-> % first e d/touch) results))) 15 | -------------------------------------------------------------------------------- /dev/clj/proto_play.clj: -------------------------------------------------------------------------------- 1 | (ns proto-play 2 | (:require [mount.tools.graph :as mg] 3 | [proto-repl-charts.graph :as proto])) 4 | 5 | (defn mount->proto [graph] 6 | (reduce (fn [g {:keys [name deps]}] 7 | (-> g 8 | (update :nodes conj name) 9 | (update :edges conj (-> deps (conj name) vec)))) 10 | {} 11 | graph)) 12 | 13 | (->> (mg/states-with-deps) 14 | mount->proto 15 | (proto/graph "a proto graph of mount states")) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tolitius/mount", 3 | "version": "0.1.16", 4 | "license": "EPL-1.0", 5 | "homepage": "https://github.com/tolitius/mount", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/tolitius/mount" 9 | }, 10 | "author": { 11 | "name": "tolitius", 12 | "url": "http://www.dotkam.com" 13 | }, 14 | "files": [ 15 | "src/*" 16 | ], 17 | "directories": { 18 | "lib": "src" 19 | }, 20 | "dependencies": { 21 | "ws": "^8.16.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/mount/tools/logger.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.tools.logger 2 | #?@(:cljs [(:require [goog.log :as glog]) 3 | (:import [goog.debug Console])])) 4 | 5 | #?(:cljs 6 | (defonce ^:dynamic *logger* 7 | (do 8 | (.setCapturing (Console.) true) 9 | (glog/getLogger "mount" nil)))) 10 | 11 | #?(:clj 12 | (defn log [msg & _] 13 | (prn msg))) 14 | 15 | #?(:cljs 16 | (defn log [msg & level] 17 | (case (first level) 18 | :error (glog/error *logger* msg nil) 19 | (glog/info *logger* msg nil)))) 20 | 21 | -------------------------------------------------------------------------------- /dev/resources/config.edn: -------------------------------------------------------------------------------- 1 | {:datomic 2 | {:uri "datomic:mem://mount"} 3 | 4 | :www {:port 4242} 5 | 6 | :h2 7 | {:classname "org.h2.Driver" 8 | :subprotocol "h2" 9 | :subname "jdbc:h2:mem:mount" 10 | :user "sa" 11 | :password ""} 12 | 13 | :rabbit 14 | {:host "192.168.1.1" 15 | :port 5672 16 | :api-port 15672 17 | :node "jabit" 18 | :exchange "foo" 19 | :queue "r-queue" 20 | :routing-key "" 21 | :auto-delete-q? true 22 | :exchange-type "direct" 23 | :vhost "/captoman" 24 | :username "guest" 25 | :password "guest"} 26 | 27 | :nrepl {:host "0.0.0.0" 28 | :port 7878}} 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | script: 4 | - boot test 5 | - boot test-cljs 6 | - boot test-cljs-advanced 7 | install: 8 | - mkdir -p ~/bin 9 | - export PATH=~/bin:$PATH 10 | 11 | - curl -L https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh -o ~/bin/boot 12 | - chmod +x ~/bin/boot 13 | jdk: openjdk8 14 | env: 15 | matrix: 16 | - BOOT_CLOJURE_VERSION=1.8.0 17 | global: 18 | - JAVA_OPTS="-Xms512m -Xmx2048m" 19 | jdk: 20 | - openjdk8 21 | cache: 22 | directories: 23 | - $HOME/.m2 24 | - $HOME/.boot/cache/bin 25 | - $HOME/.boot/cache/lib 26 | - $HOME/bin 27 | -------------------------------------------------------------------------------- /dev/clj/app/nyse.clj: -------------------------------------------------------------------------------- 1 | (ns app.nyse 2 | (:require [datomic.api :as d] 3 | [app.utils.datomic :refer [touch]])) 4 | 5 | (defn add-order [conn {:keys [ticker bid offer qty]}] 6 | @(d/transact conn [{:db/id (d/tempid :db.part/user) 7 | :order/symbol ticker 8 | :order/bid bid 9 | :order/offer offer 10 | :order/qty qty}])) 11 | 12 | (defn find-orders [conn ticker] 13 | (let [orders (d/q '[:find ?e :in $ ?ticker 14 | :where [?e :order/symbol ?ticker]] 15 | (d/db conn) ticker)] 16 | (touch conn orders))) 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test jar tag outdated install deploy tree repl 2 | 3 | clean: 4 | rm -rf target 5 | rm -rf classes 6 | 7 | jar: clean test tag 8 | clojure -A:jar 9 | 10 | test: clean 11 | clojure -X:test :patterns '[".*"]' # clojure tests 12 | # clojure -Atest-cljs -r ".*test.self.host.*" # clojure script tests 13 | # run "j8; boot test-cljs" until running cljs tests via deps.edn is fixed 14 | 15 | outdated: 16 | clojure -M:outdated 17 | 18 | tag: 19 | clojure -A:tag 20 | 21 | install: jar 22 | clojure -A:install 23 | 24 | deploy: jar 25 | clojure -A:deploy 26 | 27 | tree: 28 | mvn dependency:tree 29 | 30 | repl: 31 | clojure -A:dev -A:repl 32 | -------------------------------------------------------------------------------- /dev/clj/app/example.clj: -------------------------------------------------------------------------------- 1 | (ns app.example 2 | (:require [clojure.tools.nrepl.server :refer [start-server stop-server]] 3 | [mount.core :as mount :refer [defstate]] 4 | [app.conf :refer [config]] 5 | [app.www]) 6 | (:gen-class)) ;; for -main / uberjar (no need in dev) 7 | 8 | ;; example on creating a network REPL 9 | (defn- start-nrepl [{:keys [host port]}] 10 | (start-server :bind host :port port)) 11 | 12 | ;; nREPL is just another simple state 13 | (defstate nrepl :start (start-nrepl (:nrepl config)) 14 | :stop (stop-server nrepl)) 15 | 16 | ;; example of an app entry point 17 | (defn -main [& args] 18 | (mount/start)) 19 | -------------------------------------------------------------------------------- /test/core/mount/test/var/private_fun.clj: -------------------------------------------------------------------------------- 1 | (ns mount.test.var.private-fun 2 | (:require [clojure.test :refer [is are deftest testing use-fixtures]] 3 | [mount.core :as mount :refer [defstate]] 4 | [mount.test.var.fun-with-values :refer [private-f]])) 5 | 6 | (alter-meta! *ns* assoc ::load false) 7 | 8 | (defn in-clj-mode [f] 9 | (mount/in-clj-mode) 10 | (require :reload 'mount.test.var.fun-with-values 'mount.test.var.private-fun) 11 | (mount/start #'mount.test.var.fun-with-values/private-f) 12 | (f) 13 | (mount/stop) 14 | (mount/in-cljc-mode)) 15 | 16 | (use-fixtures :once in-clj-mode) 17 | 18 | (deftest fun-with-values 19 | (is (= (private-f 1) 42))) 20 | -------------------------------------------------------------------------------- /test/core/mount/test/printing.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.printing 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]]] 5 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 6 | [mount.core :as mount :refer [defstate]]]))) 7 | 8 | #?(:clj (alter-meta! *ns* assoc ::load false)) 9 | 10 | (defstate foo 11 | :start (do (println "Starting!") 42)) 12 | 13 | (deftest test-printing-has-no-side-effects 14 | ;; Test that printing an unstarted DerefableState does not have the 15 | ;; side-effect of starting it 16 | (println foo) 17 | (is (not= 42 foo))) 18 | -------------------------------------------------------------------------------- /dev/cljs/app/audit_log.cljs: -------------------------------------------------------------------------------- 1 | (ns app.audit-log 2 | (:require [datascript.core :as d] 3 | [cljs-time.core :refer [now]]) 4 | (:require-macros [mount.core :refer [defstate]])) 5 | 6 | (defstate log :start (d/create-conn {})) 7 | 8 | (defn audit [db source & msg] 9 | (d/transact! @db [{:db/id -1 10 | :source source 11 | :timestamp (now) 12 | :msg (apply str msg)}])) 13 | 14 | (defn find-source-logs [db source] 15 | (d/q '{:find [?t ?msg] 16 | :in [$ ?s] 17 | :where [[?e :source ?s] 18 | [?e :timestamp ?t] 19 | [?e :msg ?msg]]} 20 | @@db source)) 21 | 22 | (defn find-all-logs [db] 23 | (->> (map :e (d/datoms @@db :aevt :timestamp)) 24 | dedupe 25 | (d/pull-many @@db '[:timestamp :source :msg]))) 26 | -------------------------------------------------------------------------------- /test/cljs/tapp/audit_log.cljs: -------------------------------------------------------------------------------- 1 | (ns tapp.audit-log 2 | (:require [datascript.core :as d] 3 | [cljs-time.core :refer [now]]) 4 | (:require-macros [mount.core :refer [defstate]])) 5 | 6 | (defstate log :start (d/create-conn {})) 7 | 8 | (defn audit [db source & msg] 9 | (d/transact! @db [{:db/id -1 10 | :source source 11 | :timestamp (now) 12 | :msg (apply str msg)}])) 13 | 14 | (defn find-source-logs [db source] 15 | (d/q '{:find [?t ?msg] 16 | :in [$ ?s] 17 | :where [[?e :source ?s] 18 | [?e :timestamp ?t] 19 | [?e :msg ?msg]]} 20 | @@db source)) 21 | 22 | (defn find-all-logs [db] 23 | (->> (map :e (d/datoms @@db :aevt :timestamp)) 24 | dedupe 25 | (d/pull-many @@db '[:timestamp :source :msg]))) 26 | -------------------------------------------------------------------------------- /dev/cljs/app/websockets.cljs: -------------------------------------------------------------------------------- 1 | (ns app.websockets 2 | (:require [app.conf :refer [config]] 3 | [app.audit-log :refer [audit log]]) 4 | (:require-macros [mount.core :refer [defstate]])) 5 | 6 | (defn ws-status [ws] 7 | {:url (.-url ws) :ready-state (.-readyState ws)}) 8 | 9 | (defn connect [uri] 10 | (let [ws (js/WebSocket. uri)] 11 | (audit log :system-a "connecting to " (ws-status ws)) 12 | (set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws))) 13 | (set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws))) 14 | ws)) 15 | 16 | (defn disconnect [ws] 17 | (audit log :system-a "closing " (ws-status @ws)) 18 | (.close @ws) 19 | (audit log :system-a "disconnecting " (ws-status @ws))) 20 | 21 | (defstate system-a :start (connect (get-in @config [:system-a :uri])) 22 | :stop (disconnect system-a)) 23 | -------------------------------------------------------------------------------- /test/cljs/tapp/websockets.cljs: -------------------------------------------------------------------------------- 1 | (ns tapp.websockets 2 | (:require [tapp.conf :refer [config]] 3 | [tapp.audit-log :refer [audit log]]) 4 | (:require-macros [mount.core :refer [defstate]])) 5 | 6 | (defn ws-status [ws] 7 | {:url (.-url ws) :ready-state (.-readyState ws)}) 8 | 9 | (defn connect [uri] 10 | (let [ws (js/WebSocket. uri)] 11 | (audit log :system-a "connecting to " (ws-status ws)) 12 | (set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws))) 13 | (set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws))) 14 | ws)) 15 | 16 | (defn disconnect [ws] 17 | (audit log :system-a "closing " (ws-status @ws)) 18 | (.close @ws) 19 | (audit log :system-a "disconnecting " (ws-status @ws))) 20 | 21 | (defstate system-a :start (connect (get-in @config [:system-a :uri])) 22 | :stop (disconnect system-a)) 23 | -------------------------------------------------------------------------------- /test/core/mount/test/private_fun.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.private-fun 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]]] 5 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 6 | [mount.core :as mount :refer [defstate]]]) 7 | 8 | [mount.test.fun-with-values :refer [private-f]])) 9 | 10 | #?(:clj (alter-meta! *ns* assoc ::load false)) 11 | 12 | (use-fixtures :once 13 | #?(:cljs {:before #(mount/start #'mount.test.fun-with-values/private-f) 14 | :after mount/stop} 15 | :clj #((mount/start #'mount.test.fun-with-values/private-f) 16 | (%) 17 | (mount/stop)))) 18 | 19 | (deftest fun-with-values 20 | (is (= (@private-f 1) 42))) 21 | -------------------------------------------------------------------------------- /test/clj/tapp/nyse.clj: -------------------------------------------------------------------------------- 1 | (ns tapp.nyse 2 | (:require [mount.core :as mount :refer [defstate]] 3 | [datomic.api :as d] 4 | [clojure.tools.logging :refer [info]] 5 | [tapp.conf :refer [config]])) 6 | 7 | (alter-meta! *ns* assoc ::load false) 8 | 9 | (defn- new-connection [conf] 10 | (info "conf: " conf) 11 | (let [uri (get-in @conf [:datomic :uri])] 12 | (info "creating a connection to datomic:" uri) 13 | (d/create-database uri) 14 | (d/connect uri))) 15 | 16 | (defn disconnect [conf conn] 17 | (let [uri (get-in @conf [:datomic :uri])] 18 | (info "disconnecting from " uri) 19 | (.release @conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) 20 | (d/delete-database uri))) 21 | 22 | (defstate conn :start (new-connection config) 23 | :stop (disconnect config conn)) 24 | -------------------------------------------------------------------------------- /test/core/mount/test/helper.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.helper 2 | (:require 3 | #?@(:cljs [[mount.core :as mount :refer-macros [defstate]]] 4 | :clj [[mount.core :as mount :refer [defstate]]]))) 5 | 6 | #?(:clj (alter-meta! *ns* assoc ::load false)) 7 | 8 | (defn dval 9 | "returns a value of DerefableState without deref'ing it" 10 | [d] 11 | (-> (@@(var mount.core/meta-state) 12 | #?(:clj (.name d) 13 | :cljs (.-name d))) 14 | :inst 15 | deref)) 16 | 17 | (def forty-two (atom 42)) 18 | 19 | (defstate helper :start :started 20 | :stop (reset! forty-two :cleaned)) 21 | 22 | (def counter (atom {:a {:started 0 :stopped 0} 23 | :b {:started 0 :stopped 0} 24 | :c {:started 0 :stopped 0}})) 25 | 26 | (defn inc-counter [state status] 27 | (swap! counter update-in [state status] inc) 28 | status) 29 | -------------------------------------------------------------------------------- /src/mount/tools/macrovich.cljc: -------------------------------------------------------------------------------- 1 | (ns ^{:doc "From https://github.com/cgrand/macrovich. Licensed under EPL v1. 2 | Copyright Cristophe Grand." } 3 | mount.tools.macrovich 4 | (:refer-clojure :exclude [case])) 5 | 6 | (defmacro deftime 7 | "This block will only be evaluated at the correct time for macro definition, at other times its content 8 | are removed. 9 | For Clojure it always behaves like a `do` block. 10 | For Clojurescript/JVM the block is only visible to Clojure. 11 | For self-hosted Clojurescript the block is only visible when defining macros in the pseudo-namespace." 12 | [& body] 13 | (when #?(:clj (not (:ns &env)) :cljs (re-matches #".*\$macros" (name (ns-name *ns*)))) 14 | `(do ~@body))) 15 | 16 | (defmacro case [& {:keys [cljs clj]}] 17 | (if (contains? &env '&env) 18 | `(if (:ns ~'&env) ~cljs ~clj) 19 | (if #?(:clj (:ns &env) :cljs true) 20 | cljs 21 | clj))) 22 | -------------------------------------------------------------------------------- /test/core/mount/test_self_host.cljs: -------------------------------------------------------------------------------- 1 | (ns mount.test-self-host 2 | (:require 3 | [cljs.test :as t] 4 | 5 | mount.test.fun-with-values 6 | mount.test.private-fun 7 | mount.test.printing 8 | mount.test.parts 9 | mount.test.cleanup-dirty-states 10 | mount.test.stop-except 11 | mount.test.start-without 12 | mount.test.start-with 13 | mount.test.start-with-states 14 | )) 15 | 16 | (t/run-tests 17 | 'mount.test.fun-with-values 18 | 'mount.test.private-fun 19 | 'mount.test.printing 20 | 'mount.test.parts 21 | 'mount.test.cleanup-dirty-states 22 | ;; 'mount.test.stop-except ;; TODO: can't run with deps.edn (due to "WebSocket is not defined") 23 | ;; 'mount.test.start-with ;; boot, lein have no problems 24 | ;; 'mount.test.start-with-states ;; most likely somm misconfigured in node.. 25 | 'mount.test.start-without 26 | ) 27 | 28 | (defn run-tests [] 29 | (t/run-all-tests #"mount.test.*")) 30 | -------------------------------------------------------------------------------- /dev/cljs/app/example.cljs: -------------------------------------------------------------------------------- 1 | (ns app.example 2 | (:require [mount.core :as mount] 3 | [app.conf] 4 | [app.websockets] 5 | [app.audit-log :refer [log find-all-logs]] 6 | [cljs-time.format :refer [unparse formatters]] 7 | [hiccups.runtime :as hiccupsrt]) 8 | (:require-macros [hiccups.core :as hiccups :refer [html]])) 9 | 10 | (defn format-log-event [{:keys [timestamp source msg]}] 11 | (str (unparse (formatters :date-hour-minute-second-fraction) timestamp) 12 | " → [" (name source) "]: " msg)) 13 | 14 | (defn show-log [] 15 | (.write js/document 16 | (html [:ul (doall (for [e (find-all-logs log)] 17 | [:li (format-log-event e)]))]))) 18 | 19 | (mount/start) 20 | 21 | ;; time to establish a websocket connection before disconnecting 22 | (js/setTimeout #(mount/stop-except "#'app.audit-log/log") 500) 23 | 24 | ;; time to close a connection to show it in audit 25 | (js/setTimeout #(show-log) 1000) 26 | 27 | -------------------------------------------------------------------------------- /test/cljs/tapp/example.cljs: -------------------------------------------------------------------------------- 1 | (ns tapp.example 2 | (:require [mount.core :as mount] 3 | [tapp.conf] 4 | [tapp.websockets] 5 | [tapp.audit-log :refer [log find-all-logs]] 6 | [cljs-time.format :refer [unparse formatters]] 7 | [hiccups.runtime :as hiccupsrt]) 8 | (:require-macros [hiccups.core :as hiccups :refer [html]])) 9 | 10 | (defn format-log-event [{:keys [timestamp source msg]}] 11 | (str (unparse (formatters :date-hour-minute-second-fraction) timestamp) 12 | " → [" (name source) "]: " msg)) 13 | 14 | (defn show-log [] 15 | (.write js/document 16 | (html [:ul (doall (for [e (find-all-logs log)] 17 | [:li (format-log-event e)]))]))) 18 | 19 | (mount/start) 20 | 21 | ;; time to establish a websocket connection before disconnecting 22 | (js/setTimeout #(mount/stop-except "#'tapp.audit-log/log") 500) 23 | 24 | ;; time to close a connection to show it in audit 25 | (js/setTimeout #(show-log) 1000) 26 | 27 | -------------------------------------------------------------------------------- /test/core/mount/test/on_reload_helper.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.on-reload-helper 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.example]]) 11 | [mount.test.helper :refer [inc-counter]])) 12 | 13 | (defstate ^{:on-reload :noop} a :start (inc-counter :a :started) 14 | :stop (inc-counter :a :stopped)) 15 | 16 | (defstate ^{:on-reload :stop} b :start (inc-counter :b :started) 17 | :stop (inc-counter :b :stopped)) 18 | 19 | (defstate c :start (inc-counter :c :started) 20 | :stop (inc-counter :c :stopped)) 21 | 22 | -------------------------------------------------------------------------------- /src/mount/tools/graph.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.tools.graph) 2 | 3 | #?(:clj 4 | ;;TODO ns based for now. need to be _state_ based. or better yet need to have a real graph :) 5 | (defn- add-deps [{:keys [ns] :as state} states] 6 | (let [refers (ns-refers ns) 7 | any (->> states vals (map :var) set) 8 | deps (->> (filter (comp any val) refers) 9 | (map (comp str second)) 10 | set)] 11 | (assoc (dissoc state :ns) 12 | :deps deps)))) 13 | 14 | #?(:clj 15 | (defn- meta-with-ns [[sname {:keys [var] :as smeta}]] 16 | (let [sns (-> var meta :ns)] 17 | (assoc smeta :ns sns :name sname)))) 18 | 19 | #?(:clj 20 | (defn states-with-deps [] 21 | (let [states @@#'mount.core/meta-state] 22 | (->> (map (comp #(add-deps % states) 23 | #(select-keys % [:name :order :ns :status]) 24 | meta-with-ns) 25 | states) 26 | (sort-by :order))))) 27 | -------------------------------------------------------------------------------- /test/core/mount/test.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test 2 | (:require 3 | #?@(:cljs [[cljs.test :as t] 4 | [doo.runner :refer-macros [doo-tests]]] 5 | :clj [[clojure.test :as t]]) 6 | mount.core 7 | 8 | mount.test.fun-with-values 9 | mount.test.private-fun 10 | mount.test.parts 11 | mount.test.cleanup-dirty-states 12 | mount.test.stop-except 13 | mount.test.start-without 14 | mount.test.start-with 15 | mount.test.start-with-states 16 | mount.test.printing 17 | )) 18 | 19 | #?(:clj (alter-meta! *ns* assoc ::load false)) 20 | 21 | (mount.core/in-cljc-mode) 22 | 23 | #?(:cljs 24 | 25 | ;; (doo.runner/do-all-tests) 26 | (doo-tests 27 | 'mount.test.fun-with-values 28 | 'mount.test.private-fun 29 | 'mount.test.parts 30 | 'mount.test.cleanup-dirty-states 31 | 'mount.test.stop-except 32 | 'mount.test.start-without 33 | 'mount.test.start-with 34 | 'mount.test.start-with-states 35 | 'mount.test.printing 36 | )) 37 | 38 | (defn run-tests [] 39 | (t/run-all-tests #"mount.test.*")) 40 | -------------------------------------------------------------------------------- /dev/clj/app/utils/logging.clj: -------------------------------------------------------------------------------- 1 | (ns app.utils.logging ;; << change to your namespace/path 2 | (:require [mount.core] 3 | [robert.hooke :refer [add-hook clear-hooks]] 4 | [clojure.string :refer [split]] 5 | [clojure.tools.logging :refer [info]])) 6 | 7 | (alter-meta! *ns* assoc ::load false) 8 | 9 | (defn- f-to-action [f {:keys [status]}] 10 | (let [fname (-> (str f) 11 | (split #"@") 12 | first)] 13 | (case fname 14 | "mount.core$up" (when-not (:started status) :up) 15 | "mount.core$down" (when-not (:stopped status) :down) 16 | :noop))) 17 | 18 | (defn whatcha-doing? [action] 19 | (case action 20 | :up ">> starting" 21 | :down "<< stopping" 22 | false)) 23 | 24 | (defn log-status [f & args] 25 | (let [[state-name state] args 26 | action (f-to-action f state)] 27 | (when-let [taking-over-the-world (whatcha-doing? action)] 28 | (info (str taking-over-the-world ".. " state-name))) 29 | (apply f args))) 30 | 31 | (defonce lifecycle-fns 32 | #{#'mount.core/up 33 | #'mount.core/down}) 34 | 35 | (defn without-logging-status [] 36 | (doall (map #(clear-hooks %) lifecycle-fns))) 37 | 38 | 39 | ;; this is the one to use: 40 | 41 | (defn with-logging-status [] 42 | (without-logging-status) 43 | (doall (map #(add-hook % log-status) lifecycle-fns))) 44 | -------------------------------------------------------------------------------- /test/clj/tapp/utils/logging.clj: -------------------------------------------------------------------------------- 1 | (ns tapp.utils.logging ;; << change to your namespace/path 2 | (:require [mount.core] 3 | [robert.hooke :refer [add-hook clear-hooks]] 4 | [clojure.string :refer [split]] 5 | [clojure.tools.logging :refer [info]])) 6 | 7 | (alter-meta! *ns* assoc ::load false) 8 | 9 | (defn- f-to-action [f {:keys [status]}] 10 | (let [fname (-> (str f) 11 | (split #"@") 12 | first)] 13 | (case fname 14 | "mount.core$up" (when-not (:started status) :up) 15 | "mount.core$down" (when-not (:stopped status) :down) 16 | :noop))) 17 | 18 | (defn whatcha-doing? [action] 19 | (case action 20 | :up ">> starting" 21 | :down "<< stopping" 22 | false)) 23 | 24 | (defn log-status [f & args] 25 | (let [[state-name state] args 26 | action (f-to-action f state)] 27 | (when-let [taking-over-the-world (whatcha-doing? action)] 28 | (info (str taking-over-the-world ".. " state-name))) 29 | (apply f args))) 30 | 31 | (defonce lifecycle-fns 32 | #{#'mount.core/up 33 | #'mount.core/down}) 34 | 35 | (defn without-logging-status [] 36 | (doall (map #(clear-hooks %) lifecycle-fns))) 37 | 38 | 39 | ;; this is the one to use: 40 | 41 | (defn with-logging-status [] 42 | (without-logging-status) 43 | (doall (map #(add-hook % log-status) lifecycle-fns))) 44 | -------------------------------------------------------------------------------- /dev/clj/app/www.clj: -------------------------------------------------------------------------------- 1 | (ns app.www 2 | (:require [app.nyse :refer [add-order find-orders]] 3 | [app.db :refer [conn create-schema]] 4 | [app.conf :refer [config]] 5 | [mount.core :refer [defstate]] 6 | [cheshire.core :refer [generate-string]] 7 | [compojure.core :refer [routes defroutes GET POST]] 8 | [compojure.handler :as handler] 9 | [ring.adapter.jetty :refer [run-jetty]])) 10 | 11 | (defroutes mount-example-routes 12 | 13 | (GET "/" [] "welcome to mount sample app!") 14 | (GET "/nyse/orders/:ticker" [ticker] 15 | (generate-string (find-orders conn ticker))) 16 | 17 | (POST "/nyse/orders" [ticker qty bid offer] 18 | (let [order {:ticker ticker 19 | :bid (bigdec bid) 20 | :offer (bigdec offer) 21 | :qty (Integer/parseInt qty)}] 22 | (add-order conn order) 23 | (generate-string {:added order})))) 24 | 25 | (defn start-nyse [conn {:keys [www]}] ;; app entry point 26 | (create-schema conn) ;; just an example, usually schema would already be there 27 | (-> (routes mount-example-routes) 28 | (handler/site) 29 | (run-jetty {:join? false 30 | :port (:port www)}))) 31 | 32 | (defstate nyse-app :start (start-nyse conn config) 33 | :stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point 34 | -------------------------------------------------------------------------------- /test/core/mount/test/parts.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.parts 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.nyse :refer [conn]]]) 11 | [mount.test.helper :refer [dval]])) 12 | 13 | #?(:clj (alter-meta! *ns* assoc ::load false)) 14 | 15 | (defstate should-not-start :start (constantly 42)) 16 | 17 | #?(:clj 18 | (defn with-parts [f] 19 | (mount/start #'tapp.conf/config #'tapp.nyse/conn) 20 | (f) 21 | (mount/stop))) 22 | 23 | (use-fixtures :once 24 | #?(:cljs {:before #(mount/start #'tapp.conf/config #'tapp.audit-log/log) 25 | :after mount/stop} 26 | :clj with-parts)) 27 | 28 | #?(:clj 29 | (deftest start-only-parts 30 | (is (instance? datomic.peer.LocalConnection (dval conn))) 31 | (is (instance? mount.core.NotStartedState (dval should-not-start))))) 32 | 33 | #?(:cljs 34 | (deftest start-only-parts 35 | (is (instance? datascript.db/DB @(dval log))) 36 | (is (map? (dval config))) 37 | (is (instance? mount.core.NotStartedState (dval should-not-start))) 38 | (is (instance? mount.core.NotStartedState (dval system-a))))) 39 | -------------------------------------------------------------------------------- /dev/clj/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require [clojure.pprint :refer [pprint]] 3 | [clojure.tools.namespace.repl :as tn] 4 | [mount.core :as mount :refer [defstate]] 5 | [mount.tools.graph :refer [states-with-deps]] 6 | [app.utils.logging :refer [with-logging-status]] 7 | [app.www] 8 | [app.db :refer [conn]] 9 | [app.example] 10 | [app.nyse :refer [find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time 11 | 12 | (defn start [] 13 | (with-logging-status) 14 | (mount/start #'app.conf/config 15 | #'app.db/conn 16 | #'app.www/nyse-app 17 | #'app.example/nrepl)) ;; example on how to start app with certain states 18 | 19 | (defn stop [] 20 | (mount/stop)) 21 | 22 | (defn refresh [] 23 | (stop) 24 | (tn/refresh)) 25 | 26 | (defn refresh-all [] 27 | (stop) 28 | (tn/refresh-all)) 29 | 30 | (defn go 31 | "starts all states defined by defstate" 32 | [] 33 | (start) 34 | :ready) 35 | 36 | (defn reset 37 | "stops all states defined by defstate, reloads modified source files, and restarts the states" 38 | [] 39 | (stop) 40 | (tn/refresh :after 'dev/go)) 41 | 42 | (mount/in-clj-mode) 43 | 44 | (defn load-data-readers! 45 | "Refresh *data-readers* with readers from newly acquired dependencies." 46 | [] 47 | (#'clojure.core/load-data-readers) 48 | (set! *data-readers* (.getRawRoot #'*data-readers*))) 49 | 50 | (load-data-readers!) 51 | -------------------------------------------------------------------------------- /test/core/mount/test/start_without.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.start-without 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.conf :refer [config]] 11 | [tapp.nyse :refer [conn]] 12 | [tapp.example :refer [nrepl]]]) 13 | [mount.test.helper :refer [dval helper]])) 14 | 15 | #?(:clj (alter-meta! *ns* assoc ::load false)) 16 | 17 | #?(:clj 18 | (defn without [f] 19 | (mount/start-without #'tapp.nyse/conn #'tapp.example/nrepl) 20 | (f) 21 | (mount/stop))) 22 | 23 | (use-fixtures :once 24 | #?(:cljs {:before #(mount/start-without #'mount.test.helper/helper #'tapp.websockets/system-a) 25 | :after mount/stop} 26 | :clj without)) 27 | 28 | #?(:clj 29 | (deftest start-without-states 30 | (is (map? (dval config))) 31 | (is (instance? mount.core.NotStartedState (dval nrepl))) 32 | (is (instance? mount.core.NotStartedState (dval conn))))) 33 | 34 | #?(:cljs 35 | (deftest start-without-states 36 | (is (map? (dval config))) 37 | (is (instance? datascript.db/DB @(dval log))) 38 | (is (instance? mount.core.NotStartedState (dval helper))) 39 | (is (instance? mount.core.NotStartedState (dval system-a))))) 40 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | mount 6 | mount 7 | 0.1.23 8 | mount 9 | managing Clojure and ClojureScript app state since (reset) 10 | https://github.com/tolitius/mount 11 | 12 | 13 | Eclipse Public License 14 | http://www.eclipse.org/legal/epl-v10.html 15 | 16 | 17 | 18 | 19 | tolitius 20 | 21 | 22 | 23 | https://github.com/tolitius/mount 24 | scm:git:git://github.com/tolitius/mount.git 25 | scm:git:ssh://git@github.com/tolitius/mount.git 26 | HEAD 27 | 28 | 29 | 30 | org.clojure 31 | clojure 32 | 1.11.2 33 | provided 34 | 35 | 36 | 37 | src 38 | 39 | 40 | 41 | clojars 42 | https://repo.clojars.org/ 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/core/mount/test/cleanup_deleted_states.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.cleanup-deleted-states 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.example]]) 11 | [mount.test.helper :refer [dval helper forty-two]])) 12 | 13 | (def status (atom :a-not-started)) 14 | (defstate a :start (reset! status :a-started) 15 | :stop (reset! status :a-stopped)) 16 | 17 | #?(:clj (alter-meta! *ns* assoc ::load false)) 18 | 19 | #?(:clj 20 | (deftest cleanup-deleted-state 21 | 22 | (testing "should start all and remove/delete state from ns" 23 | (let [started (-> (mount/start) :started set)] 24 | (is (some #{"#'mount.test.cleanup-deleted-states/a"} 25 | started)) 26 | (is (= :a-started @status)) 27 | (ns-unmap 'mount.test.cleanup-deleted-states 'a) 28 | (is (nil? (resolve 'mount.test.cleanup-deleted-states/a))))) 29 | 30 | (testing "should cleanup/stop a state after it was deleted from ns" 31 | (is (empty? (:started (mount/start)))) ;; on any mount op (not necessarily on "stop") 32 | (is (= :a-stopped @status)) 33 | (is (not (some #{"#'mount.test.cleanup-deleted-states/a"} 34 | (keys @@#'mount.core/meta-state))))) 35 | 36 | (testing "should not stop it again on stop (should not be there by this point)") 37 | (is (not (some #{"#'mount.test.cleanup-deleted-states/a"} 38 | (-> (mount/stop) :stopped set)))))) 39 | 40 | ;; (t/run-tests) 41 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/mount/mount/hooks/defstate.clj: -------------------------------------------------------------------------------- 1 | (ns hooks.defstate 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn defstate [{:keys [node]}] 5 | (let [[n & args] (next (:children node)) 6 | [docs args] (if (string? (api/sexpr (first args))) 7 | [(first args) (next args)] 8 | [nil args]) 9 | m (when-let [m (first (:meta n))] 10 | (api/sexpr m)) 11 | m (if (map? m) m {}) 12 | ks (cond-> (take 1 args) 13 | (> (count args) 2) (conj (nth args 2))) 14 | invalid-key (first (remove (comp (partial contains? #{:start :stop}) api/sexpr) ks))] 15 | (cond 16 | invalid-key 17 | (api/reg-finding! 18 | {:message (str "lifecycle functions can only contain `:start` and `:stop`. illegal function found: " (api/sexpr invalid-key)) 19 | :type :mount/defstate 20 | :row (:row (meta invalid-key)) 21 | :col (:col (meta invalid-key))}) 22 | (not (contains? (set (map api/sexpr ks)) :start)) 23 | (throw (ex-info "lifecycle functions must include `:start`" {})) 24 | ((complement contains?) #{2 4} (count args)) 25 | (throw (ex-info "lifecycle functions must consist of no more than 2 pair forms: `:start` and `:stop`" {})) 26 | (and (contains? m :on-reload) (not (contains? #{:noop :stop} (:on-reload m)))) 27 | (api/reg-finding! 28 | {:message "metadata `:on-reload` key can only have value of `noop` or `stop`" 29 | :type :mount/defstate 30 | :row (:row (meta n)) 31 | :col (:col (meta n))}) 32 | :else 33 | {:node (api/list-node 34 | (cond-> [(api/token-node 'def) n] 35 | docs (conj docs) 36 | true (conj (api/list-node 37 | (list* 38 | (api/token-node 'do) 39 | args)))))}))) 40 | -------------------------------------------------------------------------------- /test/core/mount/test/var/fun_with_values.clj: -------------------------------------------------------------------------------- 1 | (ns mount.test.var.fun-with-values 2 | (:require [clojure.test :as t :refer [is are deftest testing use-fixtures]] 3 | [mount.core :as mount :refer [defstate]])) 4 | 5 | (alter-meta! *ns* assoc ::load false) 6 | 7 | (defn f [n] 8 | (fn [m] 9 | (+ n m))) 10 | 11 | (defn g [a b] 12 | (+ a b)) 13 | 14 | (defn- pf [n] 15 | (+ 41 n)) 16 | 17 | (defn fna [] 18 | 42) 19 | 20 | (defstate scalar :start 42) 21 | (defstate fun :start #(inc 41)) 22 | (defstate with-fun :start (inc 41)) 23 | (defstate with-partial :start (partial g 41)) 24 | (defstate f-in-f :start (f 41)) 25 | (defstate f-no-args-value :start (fna)) 26 | (defstate f-no-args :start fna) 27 | (defstate f-args :start g) 28 | (defstate f-value :start (g 41 1)) 29 | (defstate private-f :start pf) 30 | 31 | (defn start-states [] 32 | (mount/in-clj-mode) 33 | (require :reload 'mount.test.var.fun-with-values) 34 | (mount/start #'mount.test.var.fun-with-values/scalar 35 | #'mount.test.var.fun-with-values/fun 36 | #'mount.test.var.fun-with-values/with-fun 37 | #'mount.test.var.fun-with-values/with-partial 38 | #'mount.test.var.fun-with-values/f-in-f 39 | #'mount.test.var.fun-with-values/f-args 40 | #'mount.test.var.fun-with-values/f-no-args-value 41 | #'mount.test.var.fun-with-values/f-no-args 42 | #'mount.test.var.fun-with-values/private-f 43 | #'mount.test.var.fun-with-values/f-value)) 44 | 45 | (defn stop-states [] 46 | (mount/stop) 47 | (mount/in-cljc-mode)) 48 | 49 | (use-fixtures :once #((start-states) (%) (stop-states))) 50 | 51 | (deftest fun-with-values 52 | (is (= scalar 42)) 53 | (is (= (fun) 42)) 54 | (is (= with-fun 42)) 55 | (is (= (with-partial 1) 42)) 56 | (is (= (f-in-f 1) 42)) 57 | (is (= f-no-args-value 42)) 58 | (is (= (f-no-args) 42)) 59 | (is (= (f-args 41 1) 42)) 60 | (is (= (private-f 1) 42)) 61 | (is (= f-value 42))) 62 | -------------------------------------------------------------------------------- /dev/clj/app/db.clj: -------------------------------------------------------------------------------- 1 | (ns app.db 2 | (:require [mount.core :refer [defstate]] 3 | [datomic.api :as d] 4 | [clojure.tools.logging :refer [info]] 5 | [app.conf :refer [config]])) 6 | 7 | (defn- new-connection [conf] 8 | (info "conf: " conf) 9 | (let [uri (get-in conf [:datomic :uri])] 10 | (info "creating a connection to datomic:" uri) 11 | (d/create-database uri) 12 | (d/connect uri))) 13 | 14 | (defn disconnect [conf conn] 15 | (let [uri (get-in conf [:datomic :uri])] 16 | (info "disconnecting from " uri) 17 | (.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) 18 | (d/delete-database uri))) 19 | 20 | (defstate conn :start (new-connection config) 21 | :stop (disconnect config conn)) 22 | 23 | ;; datomic schema (staging for an example) 24 | (defn create-schema [conn] 25 | (let [schema [{:db/id #db/id [:db.part/db] 26 | :db/ident :order/symbol 27 | :db/valueType :db.type/string 28 | :db/cardinality :db.cardinality/one 29 | :db/index true 30 | :db.install/_attribute :db.part/db} 31 | 32 | {:db/id #db/id [:db.part/db] 33 | :db/ident :order/bid 34 | :db/valueType :db.type/bigdec 35 | :db/cardinality :db.cardinality/one 36 | :db.install/_attribute :db.part/db} 37 | 38 | {:db/id #db/id [:db.part/db] 39 | :db/ident :order/qty 40 | :db/valueType :db.type/long 41 | :db/cardinality :db.cardinality/one 42 | :db.install/_attribute :db.part/db} 43 | 44 | {:db/id #db/id [:db.part/db] 45 | :db/ident :order/offer 46 | :db/valueType :db.type/bigdec 47 | :db/cardinality :db.cardinality/one 48 | :db.install/_attribute :db.part/db}]] 49 | 50 | @(d/transact conn schema))) 51 | -------------------------------------------------------------------------------- /test/core/mount/test/fun_with_values.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.fun-with-values 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]]] 5 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 6 | [mount.core :as mount :refer [defstate]]]))) 7 | 8 | #?(:clj (alter-meta! *ns* assoc ::load false)) 9 | 10 | (defn f [n] 11 | (fn [m] 12 | (+ n m))) 13 | 14 | (defn g [a b] 15 | (+ a b)) 16 | 17 | (defn- pf [n] 18 | (+ 41 n)) 19 | 20 | (defn fna [] 21 | 42) 22 | 23 | (defstate scalar :start 42) 24 | (defstate fun :start #(inc 41)) 25 | (defstate with-fun :start (inc 41)) 26 | (defstate with-partial :start (partial g 41)) 27 | (defstate f-in-f :start (f 41)) 28 | (defstate f-no-args-value :start (fna)) 29 | (defstate f-no-args :start fna) 30 | (defstate f-args :start g) 31 | (defstate f-value :start (g 41 1)) 32 | (defstate private-f :start pf) 33 | 34 | (defn start-states [] 35 | (mount/start #'mount.test.fun-with-values/scalar 36 | #'mount.test.fun-with-values/fun 37 | #'mount.test.fun-with-values/with-fun 38 | #'mount.test.fun-with-values/with-partial 39 | #'mount.test.fun-with-values/f-in-f 40 | #'mount.test.fun-with-values/f-args 41 | #'mount.test.fun-with-values/f-no-args-value 42 | #'mount.test.fun-with-values/f-no-args 43 | #'mount.test.fun-with-values/private-f 44 | #'mount.test.fun-with-values/f-value)) 45 | 46 | (use-fixtures :once 47 | #?(:cljs {:before start-states 48 | :after mount/stop} 49 | :clj #((start-states) (%) (mount/stop)))) 50 | 51 | (deftest fun-with-values 52 | (is (= @scalar 42)) 53 | (is (= (@fun) 42)) 54 | (is (= @with-fun 42)) 55 | (is (= (@with-partial 1) 42)) 56 | (is (= (@f-in-f 1) 42)) 57 | (is (= @f-no-args-value 42)) 58 | (is (= (@f-no-args) 42)) 59 | (is (= (@f-args 41 1) 42)) 60 | (is (= (@private-f 1) 42)) 61 | (is (= @f-value 42))) 62 | -------------------------------------------------------------------------------- /src/mount/tools/macro.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.tools.macro 2 | (:refer-clojure :exclude [case]) 3 | #?(:cljs (:require-macros [mount.tools.macrovich :refer [deftime]]) 4 | :clj (:require [mount.tools.macrovich :refer [deftime]]))) 5 | 6 | (deftime 7 | 8 | (defmacro on-error [msg f & {:keys [fail?] 9 | :or {fail? true}}] 10 | `(mount.tools.macrovich/case 11 | :clj (try ~f 12 | (catch Throwable t# 13 | (if ~fail? 14 | (throw (RuntimeException. ~msg t#)) 15 | {:f-failed (ex-info ~msg {} t#)}))) 16 | :cljs (try ~f 17 | (catch :default t# 18 | (if ~fail? 19 | (throw (js/Error (~'str ~msg " " t#))) 20 | {:f-failed (ex-info ~msg {} t#)}))))) 21 | 22 | (defmacro throw-runtime [msg] 23 | `(throw (mount.tools.macrovich/case :clj (RuntimeException. ~msg) :cljs (~'str ~msg)))) 24 | 25 | ) 26 | 27 | ;; this is a one to one copy from https://github.com/clojure/tools.macro 28 | ;; to avoid a lib dependency for a single function 29 | 30 | (defn name-with-attributes 31 | "To be used in macro definitions. 32 | Handles optional docstrings and attribute maps for a name to be defined 33 | in a list of macro arguments. If the first macro argument is a string, 34 | it is added as a docstring to name and removed from the macro argument 35 | list. If afterwards the first macro argument is a map, its entries are 36 | added to the name's metadata map and the map is removed from the 37 | macro argument list. The return value is a vector containing the name 38 | with its extended metadata map and the list of unprocessed macro 39 | arguments." 40 | [name macro-args] 41 | (let [[docstring macro-args] (if (string? (first macro-args)) 42 | [(first macro-args) (next macro-args)] 43 | [nil macro-args]) 44 | [attr macro-args] (if (map? (first macro-args)) 45 | [(first macro-args) (next macro-args)] 46 | [{} macro-args]) 47 | attr (if docstring 48 | (assoc attr :doc docstring) 49 | attr) 50 | attr (if (meta name) 51 | (conj (meta name) attr) 52 | attr)] 53 | [(with-meta name attr) macro-args])) 54 | -------------------------------------------------------------------------------- /test/core/mount/test/on_reload.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.on-reload 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.example]]) 11 | [mount.test.helper :refer [dval helper forty-two counter inc-counter]] 12 | [mount.test.on-reload-helper :refer [a b c]])) 13 | 14 | #?(:clj (alter-meta! *ns* assoc ::load false)) 15 | 16 | #?(:clj 17 | (defn abc [f] 18 | (mount/start #'mount.test.on-reload-helper/a 19 | #'mount.test.on-reload-helper/b 20 | #'mount.test.on-reload-helper/c) 21 | (f) 22 | (mount/stop))) 23 | 24 | (use-fixtures :each 25 | #?(:cljs {:before #(mount/start #'mount.test.on-reload-helper/a 26 | #'mount.test.on-reload-helper/b 27 | #'mount.test.on-reload-helper/c) 28 | :after mount/stop} 29 | :clj abc)) 30 | 31 | #?(:clj 32 | (deftest restart-by-default 33 | (is (= '(:started) (distinct (map dval [a b c])))) 34 | (let [pre-reload @counter] 35 | (require 'mount.test.on-reload-helper :reload) 36 | 37 | ;; "a" is marked as :noop on reload 38 | ;; previous behavior left a stale reference =>>> ;; (is (instance? mount.core.NotStartedState (dval a))) ;; (!) stale reference of old a is still there somewhere 39 | (is (= :started (dval a))) ;; make sure a still has the same instance as before reload 40 | (is (= (-> pre-reload :a) ;; and the start was not called: the counter did not change 41 | (-> @counter :a))) 42 | 43 | ;; "b" is marked as :stop on reload 44 | (is (instance? mount.core.NotStartedState (dval b))) 45 | (is (= (-> pre-reload :b :started) 46 | (-> @counter :b :started))) 47 | (is (= (inc (-> pre-reload :b :stopped)) 48 | (-> @counter :b :stopped))) 49 | 50 | ;; "c" is not marked on reload, using "restart" as default 51 | (is (= :started (dval c))) 52 | (is (= (inc (-> pre-reload :c :started)) 53 | (-> @counter :c :started))) 54 | (is (= (inc (-> pre-reload :c :stopped)) 55 | (-> @counter :c :stopped)))))) 56 | -------------------------------------------------------------------------------- /test/core/mount/test/cleanup_dirty_states.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.cleanup-dirty-states 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.example]]) 11 | [mount.test.helper :refer [dval helper forty-two]])) 12 | 13 | #?(:clj (alter-meta! *ns* assoc ::load false)) 14 | 15 | #?(:clj 16 | (deftest cleanup-dirty-states 17 | (let [_ (mount/start)] 18 | (is (not (.isClosed (:server-socket (dval tapp.example/nrepl))))) 19 | (require 'tapp.example :reload) 20 | (mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance 21 | (is (not (.isClosed (:server-socket (dval tapp.example/nrepl))))) 22 | (mount/stop) 23 | (is (instance? mount.core.NotStartedState (dval tapp.example/nrepl)))))) 24 | 25 | #?(:clj 26 | (deftest restart-on-recompile 27 | (let [_ (mount/start) 28 | before (:server-socket (dval tapp.example/nrepl))] 29 | (require 'tapp.example :reload) 30 | (is (not= before (:server-socket (dval tapp.example/nrepl)))) ;; should have restarted on recompile/reload, hence different reference 31 | (mount/stop) 32 | (is (instance? mount.core.NotStartedState (dval tapp.example/nrepl)))))) 33 | 34 | #?(:clj 35 | (deftest start-on-recompile 36 | (let [_ (mount/start) 37 | before (dval tapp.conf/config)] 38 | (require 'tapp.conf :reload) 39 | (is (not (identical? before (dval tapp.conf/config)))) ;; should be a newly recompiled map 40 | (mount/stop) 41 | (is (instance? mount.core.NotStartedState (dval tapp.conf/config)))))) 42 | 43 | #?(:cljs 44 | (deftest cleanup-dirty-states 45 | (let [_ (mount/start #'mount.test.helper/helper)] 46 | (is (= :started (dval helper))) 47 | (is (= 42 @forty-two)) 48 | (.require js/goog "mount.test.helper") ;; should have run :stop of `helper` 49 | ;; (is (= :cleaned @forty-two)) ;; TODO: figure out how to reload a namespace properly 50 | ;; (is (instance? mount.core.NotStartedState (dval helper))) 51 | (mount/start #'mount.test.helper/helper) 52 | (is (= :started (dval helper))) 53 | (mount/stop) 54 | (is (instance? mount.core.NotStartedState (dval helper)))))) 55 | -------------------------------------------------------------------------------- /test/clj/tapp/example.clj: -------------------------------------------------------------------------------- 1 | (ns tapp.example 2 | (:require [datomic.api :as d] 3 | [clojure.tools.nrepl.server :refer [start-server stop-server]] 4 | [mount.core :as mount :refer [defstate]] 5 | [tapp.utils.datomic :refer [touch]] 6 | [tapp.conf :refer [config]] 7 | [tapp.nyse :as nyse])) 8 | 9 | (alter-meta! *ns* assoc ::load false) 10 | 11 | ;; example on creating a network REPL 12 | (defn- start-nrepl [{:keys [host port]}] 13 | (start-server :bind host :port port)) 14 | 15 | ;; nREPL is just another simple state 16 | (defstate nrepl :start (start-nrepl (:nrepl @config)) 17 | :stop (stop-server @nrepl)) 18 | 19 | ;; datomic schema 20 | (defn create-schema [conn] 21 | (let [schema [{:db/id #db/id [:db.part/db] 22 | :db/ident :order/symbol 23 | :db/valueType :db.type/string 24 | :db/cardinality :db.cardinality/one 25 | :db/index true 26 | :db.install/_attribute :db.part/db} 27 | 28 | {:db/id #db/id [:db.part/db] 29 | :db/ident :order/bid 30 | :db/valueType :db.type/bigdec 31 | :db/cardinality :db.cardinality/one 32 | :db.install/_attribute :db.part/db} 33 | 34 | {:db/id #db/id [:db.part/db] 35 | :db/ident :order/qty 36 | :db/valueType :db.type/long 37 | :db/cardinality :db.cardinality/one 38 | :db.install/_attribute :db.part/db} 39 | 40 | {:db/id #db/id [:db.part/db] 41 | :db/ident :order/offer 42 | :db/valueType :db.type/bigdec 43 | :db/cardinality :db.cardinality/one 44 | :db.install/_attribute :db.part/db}]] 45 | 46 | @(d/transact conn schema))) 47 | 48 | (defn add-order [ticker bid offer qty] ;; can take connection as param 49 | @(d/transact @nyse/conn [{:db/id (d/tempid :db.part/user) 50 | :order/symbol ticker 51 | :order/bid bid 52 | :order/offer offer 53 | :order/qty qty}])) 54 | 55 | 56 | (defn find-orders [ticker] ;; can take connection as param 57 | (let [orders (d/q '[:find ?e :in $ ?ticker 58 | :where [?e :order/symbol ?ticker]] 59 | (d/db @nyse/conn) ticker)] 60 | (touch @nyse/conn orders))) 61 | 62 | (defn create-nyse-schema [] 63 | (create-schema @nyse/conn)) 64 | 65 | ;; example of an app entry point 66 | (defn -main [& args] 67 | (mount/start)) 68 | -------------------------------------------------------------------------------- /test/core/mount/test/stop_except.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.stop-except 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.conf :refer [config]] 11 | [tapp.nyse :refer [conn]] 12 | [tapp.example :refer [nrepl]]]) 13 | [mount.test.helper :refer [dval helper]])) 14 | 15 | #?(:clj (alter-meta! *ns* assoc ::load false)) 16 | 17 | #?(:cljs 18 | (deftest stop-except 19 | 20 | (testing "should stop all except nrepl" 21 | (let [_ (mount/start) 22 | _ (mount/stop-except #'tapp.audit-log/log #'mount.test.helper/helper)] 23 | (is (= :started (dval helper))) 24 | (is (instance? datascript.db/DB @(dval log))) 25 | (is (instance? mount.core.NotStartedState (dval config))) 26 | (is (instance? mount.core.NotStartedState (dval system-a))) 27 | (mount/stop))) 28 | 29 | (testing "should start normally after stop-except" 30 | (let [_ (mount/start)] 31 | (is (map? (dval config))) 32 | (is (instance? js/WebSocket (dval system-a))) 33 | (is (instance? datascript.db/DB @(dval log))) 34 | (mount/stop))) 35 | 36 | (testing "should stop all normally after stop-except" 37 | (let [_ (mount/start) 38 | _ (mount/stop-except #'tapp.audit-log/log #'mount.test.helper/helper) 39 | _ (mount/stop)] 40 | (is (instance? mount.core.NotStartedState (dval config))) 41 | (is (instance? mount.core.NotStartedState (dval log))) 42 | (is (instance? mount.core.NotStartedState (dval system-a))))))) 43 | 44 | #?(:clj 45 | (deftest stop-except 46 | 47 | (testing "should stop all except nrepl" 48 | (let [_ (mount/start) 49 | _ (mount/stop-except #'tapp.nyse/conn #'tapp.conf/config)] 50 | (is (map? (dval config))) 51 | (is (instance? datomic.peer.LocalConnection (dval conn))) 52 | (is (instance? mount.core.NotStartedState (dval nrepl))) 53 | (mount/stop))) 54 | 55 | (testing "should start normally after stop-except" 56 | (let [_ (mount/start)] 57 | (is (map? (dval config))) 58 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 59 | (is (instance? datomic.peer.LocalConnection (dval conn))) 60 | (mount/stop))) 61 | 62 | (testing "should stop all normally after stop-except" 63 | (let [_ (mount/start) 64 | _ (mount/stop-except #'tapp.nyse/conn #'tapp.conf/config) 65 | _ (mount/stop)] 66 | (is (instance? mount.core.NotStartedState (dval config))) 67 | (is (instance? mount.core.NotStartedState (dval conn))) 68 | (is (instance? mount.core.NotStartedState (dval nrepl))))))) 69 | -------------------------------------------------------------------------------- /test/core/mount/test/start_with.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.start-with 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [tapp.conf :refer [config]] 11 | [tapp.nyse :refer [conn]] 12 | [tapp.example :refer [nrepl]]]) 13 | [mount.test.helper :refer [dval helper]])) 14 | 15 | #?(:clj (alter-meta! *ns* assoc ::load false)) 16 | 17 | (defstate test-conn :start 42 18 | :stop (constantly 0)) 19 | 20 | (defstate test-nrepl :start []) 21 | 22 | #?(:cljs 23 | (deftest start-with 24 | 25 | (testing "should start with substitutes" 26 | (let [_ (mount/start-with {#'tapp.websockets/system-a "system-a-sub" 27 | #'mount.test.helper/helper "helper-sub"})] 28 | (is (map? (dval config))) 29 | (is (= "helper-sub" (dval helper))) 30 | (is (= "system-a-sub" (dval system-a))) 31 | (is (instance? datascript.db/DB @(dval log))) 32 | (mount/stop))) 33 | 34 | (testing "should start normally after start-with" 35 | (let [_ (mount/start)] 36 | (is (map? (dval config))) 37 | (is (instance? datascript.db/DB @(dval log))) 38 | (is (instance? js/WebSocket (dval system-a))) 39 | (is (= 42 (dval test-conn))) 40 | (is (vector? (dval test-nrepl))) 41 | (is (= :started (dval helper))) 42 | (mount/stop))) 43 | 44 | (testing "should start-without normally after start-with" 45 | (let [_ (mount/start-without #'mount.test.start-with/test-conn 46 | #'mount.test.start-with/test-nrepl)] 47 | (is (map? (dval config))) 48 | (is (instance? datascript.db/DB @(dval log))) 49 | (is (instance? js/WebSocket (dval system-a))) 50 | (is (= :started (dval helper))) 51 | (is (instance? mount.core.NotStartedState (dval test-conn))) 52 | (is (instance? mount.core.NotStartedState (dval test-nrepl))) 53 | (mount/stop))))) 54 | 55 | #?(:clj 56 | (deftest start-with 57 | 58 | (testing "should start with substitutes" 59 | (let [_ (mount/start-with {#'tapp.nyse/conn "conn-sub" 60 | #'tapp.example/nrepl :nrepl-sub})] 61 | (is (map? (dval config))) 62 | (is (= :nrepl-sub (dval nrepl))) 63 | (is (= "conn-sub" (dval conn))) 64 | (mount/stop))) 65 | 66 | (testing "should start normally after start-with" 67 | (let [_ (mount/start)] 68 | (is (map? (dval config))) 69 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 70 | (is (instance? datomic.peer.LocalConnection (dval conn))) 71 | (is (= (dval test-conn) 42)) 72 | (is (vector? (dval test-nrepl))) 73 | (mount/stop))) 74 | 75 | (testing "should start-without normally after start-with" 76 | (let [_ (mount/start-without #'mount.test.start-with/test-conn 77 | #'mount.test.start-with/test-nrepl)] 78 | (is (map? (dval config))) 79 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 80 | (is (instance? datomic.peer.LocalConnection (dval conn))) 81 | (is (instance? mount.core.NotStartedState (dval test-conn))) 82 | (is (instance? mount.core.NotStartedState (dval test-nrepl))) 83 | (mount/stop))))) 84 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mount "0.1.17" 2 | :description "managing Clojure and ClojureScript app state since (reset)" 3 | :url "https://github.com/tolitius/mount" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :source-paths ["src"] 8 | 9 | :dependencies [] ;; for visual clarity 10 | 11 | :tach {:test-runner-ns 'mount.test-self-host 12 | :source-paths ["test/core"]} 13 | 14 | :profiles {:dev {:source-paths ["dev" "dev/clj" "test/clj"] 15 | :dependencies [[org.clojure/clojure "1.8.0"] 16 | [org.clojure/clojurescript "1.9.946"]; :classifier "aot"] 17 | [datascript "0.13.3"] 18 | [compojure "1.4.0"] 19 | [ring/ring-jetty-adapter "1.1.0"] 20 | [cheshire "5.5.0"] 21 | [hiccups "0.3.0"] 22 | [com.andrewmcveigh/cljs-time "0.3.14"] 23 | [ch.qos.logback/logback-classic "1.1.3"] 24 | [org.clojure/tools.logging "0.3.1"] 25 | [robert/hooke "1.3.0"] 26 | [org.clojure/tools.namespace "0.2.11"] 27 | [org.clojure/tools.nrepl "0.2.11"] 28 | [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]] 29 | 30 | :plugins [[lein-cljsbuild "1.1.1"] 31 | [lein-doo "0.1.6"] 32 | [lein-figwheel "0.5.0-2"] 33 | [test2junit "1.1.3"] 34 | [lein-tach "1.0.0"]] 35 | 36 | :test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit") 37 | 38 | :clean-targets ^{:protect false} [:target-path 39 | [:cljsbuild :builds :dev :compiler :output-dir] 40 | [:cljsbuild :builds :prod :compiler :output-to]] 41 | :cljsbuild { 42 | :builds {:dev 43 | {:source-paths ["src" "dev/cljs"] 44 | :figwheel true 45 | 46 | :compiler {:main app.example 47 | :asset-path "js/compiled/out" 48 | :output-to "dev/resources/public/js/compiled/mount.js" 49 | :output-dir "dev/resources/public/js/compiled/out" 50 | :optimizations :none 51 | :source-map true 52 | :source-map-timestamp true}} 53 | :test 54 | {:source-paths ["src" "dev/cljs" "test"] 55 | :compiler {:main mount.test 56 | ;; :asset-path "js/compiled/out" 57 | :output-to "dev/resources/public/js/compiled/mount.js" 58 | :output-dir "dev/resources/public/js/compiled/test" 59 | :optimizations :none 60 | :source-map true 61 | :source-map-timestamp true}} 62 | :prod 63 | {:source-paths ["src" "dev/cljs"] 64 | :compiler {:output-to "dev/resources/public/js/compiled/mount.js" 65 | :optimizations :advanced 66 | :pretty-print false}}}}} 67 | 68 | :test {:source-paths ["test/core" "test/clj" "test/cljs"]}}) 69 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | 3 | :deps {} ;; deps no deps 4 | 5 | :aliases {:dev {:extra-deps {metosin/jsonista {:mvn/version "0.3.8"} 6 | com.datomic/datomic-free {:mvn/version "0.9.5359" 7 | :exclusions [joda-time/joda-time]} 8 | org.clojure/tools.nrepl {:mvn/version "0.2.12"} 9 | org.clojure/tools.namespace {:mvn/version "0.2.11"} 10 | cheshire/cheshire {:mvn/version "5.5.0"} 11 | compojure/compojure {:mvn/version "1.5.0"} 12 | ring/ring-jetty-adapter {:mvn/version "1.1.0"} 13 | robert/hooke {:mvn/version "1.3.0"} 14 | proto-repl/proto-repl {:mvn/version "0.3.1"} 15 | proto-repl-charts/proto-repl-charts {:mvn/version "0.3.2"} 16 | nrepl/nrepl {:mvn/version "0.7.0"}}} 17 | :test {:extra-paths ["test/core" "test/clj" "test/cljs" "test/resources"] 18 | :extra-deps {com.datomic/datomic-free {:mvn/version "0.9.5359" 19 | :exclusions [joda-time/joda-time]} 20 | org.clojure/tools.nrepl {:mvn/version "0.2.12"} 21 | robert/hooke {:mvn/version "1.3.0"} 22 | org.clojure/tools.logging {:mvn/version "1.3.0"} 23 | io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" 24 | :sha "e7660458ce25bc4acb4ccc3e2415aae0a4907198"}} 25 | :main-opts ["-m" "cognitect.test-runner"] 26 | :exec-fn cognitect.test-runner.api/test} 27 | :test-cljs {:extra-paths ["test/core" "test/cljs" "test/resources"] 28 | :extra-deps {org.clojure/clojure {:mvn/version "1.8.0"} 29 | org.clojure/clojurescript {:mvn/version "1.7.228"} 30 | com.andrewmcveigh/cljs-time {:mvn/version "0.3.14"} 31 | hiccups/hiccups {:mvn/version "0.3.0"} 32 | datascript/datascript {:mvn/version "0.15.0"} 33 | olical/cljs-test-runner {:mvn/version "3.8.1"}} 34 | :main-opts ["-m" "cljs-test-runner.main"]} 35 | :repl {:extra-paths ["dev/clj"] 36 | :extra-deps {cider/cider-nrepl {:mvn/version "0.22.4"} 37 | org.clojure/tools.logging {:mvn/version "1.2.4"} 38 | com.bhauman/rebel-readline {:mvn/version "0.1.4"}} 39 | :main-opts ["-e" "(require 'dev)(in-ns 'dev)" 40 | "-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]" 41 | "-i" "-f" "rebel-readline.main/-main"]} 42 | :outdated {:extra-deps {olical/depot {:mvn/version "2.0.1"}} 43 | :main-opts ["-m" "depot.outdated.main" "-a" "outdated"]} 44 | :tag {:extra-deps {tolitius/tag {:mvn/version "0.1.7"}} 45 | :main-opts ["-m" "tag.core" "tolitius/mount" "managing Clojure and ClojureScript app state since (reset)"]} 46 | :jar {:extra-deps {seancorfield/depstar {:mvn/version "1.1.128"}} 47 | :extra-paths ["target/about"] 48 | :main-opts ["-m" "hf.depstar.jar" "target/mount.jar" "--exclude" "clojure/core/specs/alpha.*"]} 49 | :deploy {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}} 50 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "target/mount.jar"]} 51 | :install {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}} 52 | :main-opts ["-m" "deps-deploy.deps-deploy" "install" "target/mount.jar"]}}} 53 | -------------------------------------------------------------------------------- /doc/runtime-arguments.md: -------------------------------------------------------------------------------- 1 | ## Passing Runtime Arguments 2 | 3 | This example lives in the `with-args` branch. If you'd like to follow along: 4 | 5 | ```bash 6 | $ git checkout with-args 7 | Switched to branch 'with-args' 8 | ``` 9 | 10 | ## Start with args 11 | 12 | In order to pass runtime arguments, these could be `-this x -that y` params or `-Dparam=` or 13 | just a path to an external configuration file, `mount` has a special `start-with-args` function: 14 | 15 | ```clojure 16 | (defn -main [& args] 17 | (mount/start-with-args args)) 18 | ``` 19 | 20 | Most of the time it is better to parse args before they "get in", so usually accepting args would look something like: 21 | 22 | ```clojure 23 | (defn -main [& args] 24 | (mount/start-with-args 25 | (parse-args args))) 26 | ``` 27 | 28 | where the `parse-args` is an app specific function. 29 | 30 | ### Reading arguments 31 | 32 | Once the arguments are passed to the app, they are available via: 33 | 34 | ```clojure 35 | (mount/args) 36 | ``` 37 | 38 | Which, unless the arguments were parsed or modified in the `-main` function, 39 | will return the original `args` that were passed to `-main`. 40 | 41 | ### "Reading" example 42 | 43 | Here is an [example app](https://github.com/tolitius/mount/blob/with-args/test/app/app.clj) that takes `-main` arguments 44 | and parses them with [tools.cli](https://github.com/clojure/tools.cli): 45 | 46 | ```clojure 47 | ;; "any" regular function to pass arguments 48 | (defn parse-args [args] 49 | (let [opts [["-d" "--datomic-uri [datomic url]" "Datomic URL" 50 | :default "datomic:mem://mount"] 51 | ["-h" "--help"]]] 52 | (-> (parse-opts args opts) 53 | :options))) 54 | 55 | ;; example of an app entry point with arguments 56 | (defn -main [& args] 57 | (mount/start-with-args 58 | (parse-args args))) 59 | ``` 60 | 61 | For the example sake the app reads arguments in two places: 62 | 63 | * [inside](https://github.com/tolitius/mount/blob/with-args/test/app/nyse.clj#L17) a `defstate` 64 | 65 | ```clojure 66 | (defstate conn :start (new-connection (mount/args)) 67 | :stop (disconnect (mount/args) conn)) 68 | ``` 69 | 70 | * and from "any" [other place](https://github.com/tolitius/mount/blob/with-args/test/app/config.clj#L8) within a function: 71 | 72 | ```clojure 73 | (defn load-config [path] 74 | ;; ... 75 | (if (:help (mount/args)) 76 | (info "\n\nthis is a sample mount app to demo how to pass and read runtime arguments\n")) 77 | ;; ...) 78 | ``` 79 | 80 | ### "Uber" example 81 | 82 | In order to demo all of the above, we'll build an uberjar: 83 | 84 | ```bash 85 | $ lein do clean, uberjar 86 | ... 87 | Created .. mount/target/mount-0.1.5-SNAPSHOT-standalone.jar 88 | ``` 89 | 90 | Since we have a default for a Datomic URI, it'll work with no arguments: 91 | 92 | ```bash 93 | $ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar 94 | 95 | 22:12:03.290 [main] INFO mount - >> starting.. app-config 96 | 22:12:03.293 [main] INFO mount - >> starting.. conn 97 | 22:12:03.293 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount 98 | 22:12:03.444 [main] INFO mount - >> starting.. nrepl 99 | ``` 100 | 101 | Now let's ask it to help us: 102 | 103 | ```bash 104 | $ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar --help 105 | 106 | 22:13:48.798 [main] INFO mount - >> starting.. app-config 107 | 22:13:48.799 [main] INFO app.config - 108 | 109 | this is a sample mount app to demo how to pass and read runtime arguments 110 | 111 | 22:13:48.801 [main] INFO mount - >> starting.. conn 112 | 22:13:48.801 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount 113 | 22:13:48.946 [main] INFO mount - >> starting.. nrepl 114 | ``` 115 | 116 | And finally let's connect to the Single Malt Database. It's Friday.. 117 | 118 | ```bash 119 | $ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar -d datomic:mem://single-malt-database 120 | 121 | 22:16:10.733 [main] INFO mount - >> starting.. app-config 122 | 22:16:10.737 [main] INFO mount - >> starting.. conn 123 | 22:16:10.737 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://single-malt-database 124 | 22:16:10.885 [main] INFO mount - >> starting.. nrepl 125 | ``` 126 | 127 | ### Other usecases 128 | 129 | Depending the requirements, these runtime arguments could take different shapes of forms. You would have a full control 130 | over what is passed to the app, the same way you have it without mount through `-main [& args]`. 131 | -------------------------------------------------------------------------------- /test/core/mount/test/start_with_states.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.start-with-states 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [mount.core :as mount :refer-macros [defstate]] 5 | [tapp.websockets :refer [system-a]] 6 | [tapp.conf :refer [config]] 7 | [tapp.audit-log :refer [log]]] 8 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 9 | [mount.core :as mount :refer [defstate]] 10 | [clojure.tools.nrepl.server :refer [start-server stop-server]] 11 | [tapp.conf :refer [config]] 12 | [tapp.nyse :refer [conn]] 13 | [tapp.example :refer [nrepl]]]) 14 | [mount.test.helper :refer [dval helper]])) 15 | 16 | #?(:clj (alter-meta! *ns* assoc ::load false)) 17 | 18 | (defstate test-conn :start 42 19 | :stop (constantly 0)) 20 | 21 | (defstate test-nrepl :start []) 22 | 23 | (def swap-conn {:start (fn [] 42) 24 | :stop #(println "stopping test-conn-state")}) 25 | #?(:clj 26 | (def swap-nrepl {:start #(start-server :bind "localhost" :port 3442) 27 | :stop #(stop-server @nrepl)}) 28 | :cljs 29 | (def swap-nrepl {:start (fn [] :nrepl) 30 | :stop (fn [] :stopped-nrepl)})) 31 | 32 | #?(:cljs 33 | (deftest start-with-states 34 | 35 | (testing "should start with substitutes" 36 | (let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn 37 | #'mount.test.helper/helper swap-nrepl})] 38 | (is (map? (dval config))) 39 | (is (= (:nrepl (dval helper)))) 40 | (is (= (dval system-a) 42)) 41 | (is (instance? datascript.db/DB @(dval log))) 42 | (mount/stop))) 43 | 44 | #_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states 45 | (let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn})] 46 | (is (instance? mount.core.NotStartedState (dval test-conn))) 47 | (is (= 42 (dval system-a))) 48 | (mount/stop))) 49 | 50 | (testing "should start normally after start-with-states" 51 | (let [_ (mount/start)] 52 | (is (map? (dval config))) 53 | (is (instance? datascript.db/DB @(dval log))) 54 | (is (instance? js/WebSocket (dval system-a))) 55 | (is (= 42 (dval test-conn))) 56 | (is (vector? (dval test-nrepl))) 57 | (is (= :started (dval helper))) 58 | (mount/stop))) 59 | 60 | (testing "should start-without normally after start-with-states" 61 | (let [_ (mount/start-without #'mount.test.start-with-states/test-conn 62 | #'mount.test.start-with-states/test-nrepl)] 63 | (is (map? (dval config))) 64 | (is (instance? datascript.db/DB @(dval log))) 65 | (is (instance? js/WebSocket (dval system-a))) 66 | (is (= :started (dval helper))) 67 | (is (instance? mount.core.NotStartedState (dval test-conn))) 68 | (is (instance? mount.core.NotStartedState (dval test-nrepl))) 69 | (mount/stop))))) 70 | 71 | #?(:clj 72 | (deftest start-with-states 73 | 74 | (testing "should start with substitutes" 75 | (let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn 76 | #'tapp.example/nrepl swap-nrepl})] 77 | (is (map? (dval config))) 78 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 79 | (is (= (dval conn) 42)) 80 | (mount/stop))) 81 | 82 | #_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states 83 | (let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn})] 84 | (is (instance? mount.core.NotStartedState (dval test-conn))) 85 | (is (= (dval conn) 42)) 86 | (mount/stop))) 87 | 88 | (testing "should start normally after start-with-states" 89 | (let [_ (mount/start)] 90 | (is (map? (dval config))) 91 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 92 | (is (instance? datomic.peer.LocalConnection (dval conn))) 93 | (is (= (dval test-conn) 42)) 94 | (is (vector? (dval test-nrepl))) 95 | (mount/stop))) 96 | 97 | (testing "should start-without normally after start-with-states" 98 | (let [_ (mount/start-without #'mount.test.start-with-states/test-conn 99 | #'mount.test.start-with-states/test-nrepl)] 100 | (is (map? (dval config))) 101 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 102 | (is (instance? datomic.peer.LocalConnection (dval conn))) 103 | (is (instance? mount.core.NotStartedState (dval test-conn))) 104 | (is (instance? mount.core.NotStartedState (dval test-nrepl))) 105 | (mount/stop))))) 106 | -------------------------------------------------------------------------------- /doc/clojurescript.md: -------------------------------------------------------------------------------- 1 | ## Managing state in ClojureScript 2 | 3 | - [The "Why"](#the-why) 4 | - [Mount Modes](#mount-modes) 5 | - [Just Clojure Mode](#just-clojure-mode) 6 | - [Clojure and ClojureScript Mode](#clojure-and-clojurescript-mode) 7 | - [Mounting that ClojureScript](#mounting-that-clojurescript) 8 | - [Using States](#using-states) 9 | - [Thanks](#thanks) 10 | 11 | In case you need to manage state in ClojureScript using mount, _all_ the mount Clojure features are supported in ClojureScript. 12 | Which means all the mount Clojure [documentation](../README.md) is the mount ClojureScript documentation. 13 | 14 | With a slight change in [_mode_](clojurescript.md#mount-modes) ( no change in _mood_ though, just the _mode_ :)). 15 | 16 | ### The "Why" 17 | 18 | Since [reader conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) were added in Clojure 1.7, 19 | it became a lot easier to target both platforms with lots of code reuse. You might have noticed 20 | that most of mount code lives in `.cljc` files. 21 | 22 | The way mount is designed it "mounts" itself to a solid Clojure [namespace API](http://clojure.org/namespaces), 23 | and while `.cljc` helps a lot with targeting Clojure and ClojureScript, JavaScript VM is vastly different from JVM. 24 | Since JavaScript mostly tagrets browsers, mobile devices and IoT, 25 | it is quite important to [compress](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation) the final result. 26 | 27 | Which means that Clojure namespaces API are not that well supported in ClojureScript, since they get renamed and optimized 28 | during compilation + of course no native namespace support on the JavaScript side 29 | (but that is somewhat solved with [Google Closure](https://closure-library.googlecode.com/git-history/docs/local_closure_goog_base.js.source.html#line428)). 30 | 31 | But. When developing an application in Clojure and ClojureScript, it would only make sense if the API for any library 32 | would be _identical_ for both platforms. It should be transparent for developers whether they use a library in Clojure or ClojureScript. 33 | It is not possible for all libraries (i.e. concurrency, reified Vars, etc.), but we should try to make it possible for most. 34 | 35 | ### Mount Modes 36 | 37 | Mount has two modes `clj` and `cljc`. 38 | 39 | #### Just Clojure Mode 40 | 41 | `clj` mode is _default_, and all the APIs are exactly the same as they are in the mount Clojure [documentation](../README.md). 42 | 43 | #### Clojure _and_ ClojureScript Mode 44 | 45 | `cljc` is not a default mode, but it is easy to switch to: 46 | 47 | To switch Mount into this mode do: 48 | 49 | ```clojure 50 | (mount/in-cljc-mode) 51 | ``` 52 | 53 | anywhere before a call to `(mount/start)`, usually at the entry point of an app: in the `-main`, web handler, etc. 54 | 55 | This sets mount into the `cljc` mode. In this mode mount supports _both_: Clojure and ClojureScript with one difference 56 | from the default `clj` mode: 57 | 58 | > all states are "_derefable_" 59 | 60 | which means in order to use them, you'd need to `@` it. That's where the difference between the two modes ends. 61 | 62 | Again, `cljc` mode API is _consistent across both_ Clojure and ClojureScript. 63 | 64 | While initially it may sound strange, this approach has very nice properties: 65 | 66 | * Mentally something that you deref (`@`) is associated with a state behind it 67 | * The whole system may start lazily without an explicit call `(mount/start)` 68 | * States may have watchers which is just an idea at this point, but it could be quite useful 69 | 70 | No need to call `(mount/in-cljc-mode)` on ClojureScript side, it is only called once on the server (Clojure) side. 71 | 72 | _note: `(mount/in-cljc-mode)` does not require the code to be `.cljc`, just a geeky name to convey the support for both modes: Clojure and ClojureScript_ 73 | 74 | Now that the theory is laid out... 75 | 76 | ### Mounting that ClojureScript 77 | 78 | Let's look at the example [ClojureScript app](../dev/cljs/app) that uses mount to manage several states: 79 | 80 | * [Datascript](https://github.com/tonsky/datascript) Database 81 | * Websocket Connection 82 | * Configuration 83 | 84 | In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with: 85 | 86 | ``` 87 | $ boot cljs-example 88 | Started Jetty on http://localhost:3000 89 | nREPL server started on port 64412 on host 127.0.0.1 - nrepl://127.0.0.1:64412 90 | Adding :require adzerk.boot-cljs-repl to mount.cljs.edn... 91 | Compiling ClojureScript... 92 | • mount.js 93 | ``` 94 | 95 | And just open a browser at [http://localhost:3000](http://localhost:3000): 96 | 97 | 98 | 99 | The flow behind the app is quite simple: 100 | 101 | * load config 102 | * open a WebSocket connection 103 | * keep an audit log in Datascript 104 | * call `(mount/stop)` to disconnect 105 | 106 | #### Using States 107 | 108 | A good example of derefing state is here in [websockets.cljs](https://github.com/tolitius/mount/blob/0825ad2ed085b73b7ae989b4382ce4e0376e4be3/dev/cljs/app/websockets.cljs#L21): 109 | 110 | ```clojure 111 | 112 | (ns app.websockets 113 | (:require [app.conf :refer [config]] 114 | [app.audit-log :refer [audit log]]) 115 | (:require-macros [mount.core :refer [defstate]])) 116 | 117 | ;; ... 118 | 119 | (defstate system-a :start (connect (get-in @config [:system-a :uri])) 120 | :stop (disconnect system-a)) 121 | ``` 122 | 123 | notice how config is deferef'ed `@config` in order to use its state. It of course does not have to be deref'ed here, and 124 | can be just passed along to the `connect` function to be `@`ed there instead. 125 | 126 | ### Thanks 127 | 128 | I'd like to thank these good people for brainstorming and supporting the idea of Mount in ClojureScript universe: 129 | 130 | [@DomKM](https://github.com/DomKM), [@yogthos](https://github.com/yogthos) and [@edvorg](https://github.com/edvorg) 131 | -------------------------------------------------------------------------------- /doc/uberjar.md: -------------------------------------------------------------------------------- 1 | ## Creating Reloadable Uberjar'able App 2 | 3 | This example lives in the `uberjar` branch. If you'd like to follow along: 4 | 5 | ```bash 6 | $ git checkout uberjar 7 | Switched to branch 'uberjar' 8 | ``` 9 | 10 | ### App state 11 | 12 | Here is an example [app](https://github.com/tolitius/mount/tree/uberjar/test/app) that has these states: 13 | 14 | ```clojure 15 | 22:15:17.175 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.config/app-config 16 | 22:15:17.176 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.db/conn 17 | 22:15:17.196 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.www/nyse-app 18 | 22:15:17.199 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app/nrepl 19 | ``` 20 | 21 | where `nyse-app` is _the_ app. It has the usual routes: 22 | 23 | ```clojure 24 | (defroutes mount-example-routes 25 | 26 | (GET "/" [] "welcome to mount sample app!") 27 | (GET "/nyse/orders/:ticker" [ticker] 28 | (generate-string (find-orders ticker))) 29 | 30 | (POST "/nyse/orders" [ticker qty bid offer] 31 | (add-order ticker (bigdec bid) (bigdec offer) (Integer/parseInt qty)) 32 | (generate-string {:added {:ticker ticker 33 | :qty qty 34 | :bid bid 35 | :offer offer}}))) 36 | ``` 37 | 38 | and the reloadable state: 39 | 40 | ```clojure 41 | (defn start-nyse [{:keys [www]}] 42 | (-> (routes mount-example-routes) 43 | (handler/site) 44 | (run-jetty {:join? false 45 | :port (:port www)}))) 46 | 47 | (defstate nyse-app :start (start-nyse app-config) 48 | :stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point 49 | ``` 50 | 51 | In order not to block, and being reloadable, the Jetty server is started in the "`:join? false`" mode which starts the server, 52 | and just returns a reference to it, so it can be easily stopped by `(.stop server)` 53 | 54 | ### "Uberjar is the :main" 55 | 56 | In order for a standalone jar to run, it needs an entry point. This sample app [has one](https://github.com/tolitius/mount/blob/uberjar/test/app/app.clj#L16): 57 | 58 | ```clojure 59 | ;; example of an app entry point 60 | (defn -main [& args] 61 | (mount/start)) 62 | ``` 63 | 64 | And some usual suspects from `project.clj`: 65 | 66 | ```clojure 67 | ;; "test" is in sources here to just "demo" the uberjar without poluting mount "src" 68 | :uberjar {:source-paths ["test/app"] 69 | :main app 70 | :aot :all}}) 71 | ``` 72 | 73 | ### REPL time 74 | 75 | ```clojure 76 | $ lein do clean, repl 77 | 78 | user=> (dev)(reset) 79 | 22:15:17.175 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.config/app-config 80 | 22:15:17.176 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.db/conn 81 | 22:15:17.196 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.www/nyse-app 82 | 22:15:17.199 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app/nrepl 83 | dev=> 84 | ``` 85 | 86 | Jetty server is started and ready to roll. And everything is still reloadable: 87 | 88 | ```clojure 89 | dev=> (reset) 90 | 22:19:49.436 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app/nrepl 91 | 22:19:49.436 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app.db/conn 92 | 22:19:49.437 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app.config/app-config 93 | 94 | :reloading () 95 | 96 | 22:19:49.471 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app.config/app-config 97 | 22:19:49.472 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app.db/conn 98 | 22:19:49.490 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app/nrepl 99 | ``` 100 | 101 | notice that a web server `#'app.www/nyse-app` was not restarted. This is done by choice, not to get an occasional `java.net.BindException: Address already in use` when reloading. We can skip restarting any state or states by using `(stop-except)`. Here is from [dev.clj](https://github.com/tolitius/mount/blob/uberjar/dev/dev.clj#L25): 102 | 103 | ```clojure 104 | (defn stop [] 105 | (mount/stop-except #'app.www/nyse-app)) 106 | ``` 107 | 108 | This is not really related to uberjaring, but it is a nice optional property. Here is more documentation on [stopping an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states). 109 | 110 | ### Packaging one super uber jar 111 | 112 | ```clojure 113 | $ lein do clean, uberjar 114 | ... 115 | Created /path-to/mount/target/mount-0.1.5-SNAPSHOT-standalone.jar ;; your version may vary 116 | ``` 117 | 118 | Let's give it a spin: 119 | 120 | ```bash 121 | $ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar 122 | ... 123 | 22:25:35.303 [main] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:4242 124 | 22:25:35.303 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:4242 125 | 22:25:35.304 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED org.eclipse.jetty.server.Server@ab2009f 126 | ``` 127 | 128 | Up and running on port `:4242`: 129 | 130 | 131 | 132 | See if we have any orders: 133 | 134 | 135 | 136 | we don't. let's put something into Datomic: 137 | 138 | ```clojure 139 | $ curl -X POST -d "ticker=GOOG&qty=100&bid=665.51&offer=665.59" "http://localhost:4242/nyse/orders" 140 | {"added":{"ticker":"GOOG","qty":"100","bid":"665.51","offer":"665.59"}} 141 | ``` 142 | 143 | now we should: 144 | 145 | 146 | 147 | ### Choices 148 | 149 | There are multiple ways to start a web app. This above is the most straighforward one: start server / stop server. 150 | 151 | But depending on the requirements / architecture, the app can also have an entry point to `(mount/start)` 152 | via something like [:ring :init](https://github.com/weavejester/lein-ring#general-options)). Or the `(mount/start)` 153 | can go into the handler function, etc. 154 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.19 2 | ###### Wed Aug 21 22:43:40 2024 -0400 3 | 4 | * add `clojure-kondo` 5 | 6 | ## 0.1.17 7 | ###### Mon Dec 19 15:17:21 2022 -0500 8 | 9 | * 2d050e9 fix: swap-states non string rollback (thanks to [egg-juxt](https://github.com/egg-juxt)) 10 | 11 | ## 0.1.16 12 | ###### Mon Jan 28 10:34:26 2019 -0500 13 | 14 | * fb52f79 prevent reloading of mount.core ns ([106](https://github.com/tolitius/mount/issues/106)) 15 | * c5f3e4c current-state should return Derefable on :cljc ([104](https://github.com/tolitius/mount/issues/104)) 16 | * c2687d1 silent `*logger*` warning in latest ClojureScript([101](https://github.com/tolitius/mount/issues/101)) 17 | * bb23747 switch form circle to travis 18 | 19 | ## 0.1.14 20 | ###### Thu Oct 25 18:34:22 2018 -0400 21 | 22 | * cljs: throw js/Error not just a string ([#100](https://github.com/tolitius/mount/issues/100)) 23 | * add ^{:on-lazy-start :throw} ([#95](https://github.com/tolitius/mount/issues/95) and [#99](https://github.com/tolitius/mount/issues/99)) 24 | * self hosted ClojureScript support ([#85](https://github.com/tolitius/mount/issues/85) and [#97](https://github.com/tolitius/mount/issues/97)) 25 | 26 | ## 0.1.12 27 | ###### Sat Feb 10 14:53:04 2018 -0500 28 | 29 | * remove ref to old state alias in `swap-states` ([#89](https://github.com/tolitius/mount/issues/89)) 30 | * `:on-reload :noop` should not leave stale references 31 | * `stop-except` & `start-without` to take all states ([#81](https://github.com/tolitius/mount/issues/81)) 32 | * goog.log/error for logging cljs errors ([#63](https://github.com/tolitius/mount/issues/63)) 33 | * on failure (down) reports and cleans up vs. just "throw" ([#63](https://github.com/tolitius/mount/issues/63)) 34 | * do not start a `DerefableState` as a side-effect of printing ([#76](https://github.com/tolitius/mount/issues/76)) 35 | 36 | ## 0.1.11 37 | ###### Fri Dec 2 18:09:51 2016 -0600 38 | 39 | * opening `(find-all-states)` 40 | * stop accepts a collection of states (in addition to varargs) ([#69](https://github.com/tolitius/mount/issues/69)) 41 | * swap-states and start-with-states take values ([#68](https://github.com/tolitius/mount/issues/68)) 42 | * adding `(running-states)` that returns a set 43 | * `(mount/start #{})` is a noop ([#65](https://github.com/tolitius/mount/issues/65)) 44 | * removing log on state discovery 45 | * adding a restart listener with watchers 46 | 47 | ## 0.1.10 48 | ###### Sun Feb 28 18:20:52 2016 -0500 49 | 50 | * runtime args are now initialized to `{}` (rather than to `:no-args`) 51 | * composing states on mount start ([#47](https://github.com/tolitius/mount/issues/47)) 52 | * removing `:suspend` and `:resume` ([#46](https://github.com/tolitius/mount/issues/46)) 53 | 54 | ## 0.1.9 55 | ###### Sun Jan 31 15:47:19 2016 -0500 56 | 57 | * `:on-reload` #{:noop :stop :restart} ([#36](https://github.com/tolitius/mount/issues/36)) 58 | * swapping states with values ([#45](https://github.com/tolitius/mount/issues/45)) 59 | * `(mount.core/system)` experiment is refactored to [Yurt](https://github.com/tolitius/yurt) 60 | * cleaning up deleted states ([#42](https://github.com/tolitius/mount/issues/42)) 61 | * refactoring mount sample app (4 states, stateless fns) 62 | * refactoring cljs logging to Closure (goog.log) 63 | 64 | ## 0.1.8 65 | ###### Mon Jan 4 14:09:17 2016 -0500 66 | 67 | * pluging in [boot-check](https://github.com/tolitius/boot-check) 68 | * refactoring reader conditionals out of cljs exceptions macro (thx [@DomKM](https://github.com/DomKM)) 69 | * riding on [boot-stripper](https://github.com/tolitius/boot-stripper) 70 | * mount.tools.graph: [states-with deps](https://github.com/tolitius/mount/blob/0.1.8/src/mount/tools/graph.cljc#L20) 71 | * fixing bug in start-with-args (thx [@malchmih](https://github.com/malchmih)) ([#30](https://github.com/tolitius/mount/issues/30)) 72 | * states with no :stop are restarted on ns recompile ([#22](https://github.com/tolitius/mount/issues/22)), ([#25](https://github.com/tolitius/mount/issues/25)), ([#26](https://github.com/tolitius/mount/issues/26)) 73 | * restarting a state on ns recompile ([#22](https://github.com/tolitius/mount/issues/22)) 74 | 75 | ## 0.1.7 76 | ###### Mon Dec 21 20:52:31 2015 -0500 77 | 78 | * making mount [boot](https://github.com/boot-clj/boot)'iful 79 | * cljs `:classifier "aot"` is fixed by boot ([#23](https://github.com/tolitius/mount/issues/23)) 80 | * refactoring example app: + www 81 | * stopping/cleaning state when its namespace is recompiled ([#22](https://github.com/tolitius/mount/issues/22)) 82 | 83 | ## 0.1.6 84 | ###### Thu Dec 10 00:40:18 2015 -0500 85 | 86 | * adding full ClojureScript support ([#10](https://github.com/tolitius/mount/issues/10)) 87 | * removing all the dependencies (`:dependencies []`) 88 | * adding a sample [cljs app](https://github.com/tolitius/mount/blob/1ac28981a6a63a103a9057fd34a338c37acb913b/doc/clojurescript.md#mounting-that-clojurescript) (datascript, websockets) 89 | * introducting `cljc` and `clj` [modes](https://github.com/tolitius/mount/blob/1ac28981a6a63a103a9057fd34a338c37acb913b/doc/clojurescript.md#mount-modes) 90 | * `DerefableState`: states are _optionally_ derefable (via `IDeref`) 91 | * removing dependency on var's meta 92 | 93 | ## 0.1.5 94 | ###### Tue Dec 1 08:58:26 2015 -0500 95 | 96 | * cleaning up stale states ([#18](https://github.com/tolitius/mount/issues/18)) 97 | * adding ns to state order to avoid collisions 98 | * consolidating status ([#19](https://github.com/tolitius/mount/issues/19)) 99 | * lifecycle fns take fns and values ([#20](https://github.com/tolitius/mount/issues/20)) 100 | * not retaining heads in side-effectful iterations ([#17](https://github.com/tolitius/mount/issues/17)) 101 | * logging AOP for REPL examples ([#15](https://github.com/tolitius/mount/issues/15)) 102 | * lifecycle functions return states touched ([#15](https://github.com/tolitius/mount/issues/15)) 103 | * removing tools.logging dep ([#15](https://github.com/tolitius/mount/issues/15)) 104 | * removing tools.macro dep 105 | * removing tools.namespace dep 106 | 107 | ## 0.1.4 108 | ###### Sat Nov 21 15:31:13 2015 -0500 109 | 110 | * [suspendable states](https://github.com/tolitius/mount#suspending-and-resuming) 111 | * [stop-except](https://github.com/tolitius/mount#stop-an-application-except-certain-states) 112 | 113 | ## 0.1.3 114 | ###### Wed Nov 18 00:43:44 2015 -0500 115 | 116 | * states-with-deps [[#12](https://github.com/tolitius/mount/issues/12)] 117 | * mount => mount.core [[#11](https://github.com/tolitius/mount/issues/11)] 118 | * states without `:stop` still become `NotStartedState` on `(mount/stop)` 119 | 120 | ## 0.1.2 121 | ###### Sun Nov 15 23:15:20 2015 -0500 122 | 123 | * [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations) 124 | * [start-without](https://github.com/tolitius/mount#start-an-application-without-certain-states) 125 | 126 | ## 0.1.1 127 | ###### Sat Nov 14 16:40:38 2015 -0500 128 | 129 | * [support for runtime arguments](https://github.com/tolitius/mount#runtime-arguments) 130 | 131 | ## 0.1.0 132 | ###### Fri Nov 13 17:00:43 2015 -0500 133 | 134 | * defstate/start/stop 135 | * welcome mount 136 | -------------------------------------------------------------------------------- /test/core/mount/test/composable_fns.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.test.composable-fns 2 | (:require 3 | #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] 4 | [clojure.set :refer [intersection]] 5 | [mount.core :refer [only except swap swap-states with-args] :as mount :refer-macros [defstate]] 6 | [tapp.websockets :refer [system-a]] 7 | [tapp.conf :refer [config]] 8 | [tapp.audit-log :refer [log]]] 9 | :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] 10 | [clojure.set :refer [intersection]] 11 | [clojure.tools.nrepl.server :refer [start-server stop-server]] 12 | [mount.core :as mount :refer [defstate only except swap swap-states with-args]] 13 | [tapp.conf :refer [config]] 14 | [tapp.nyse :refer [conn]] 15 | [tapp.example :refer [nrepl]]]) 16 | [mount.test.helper :refer [dval helper]])) 17 | 18 | #?(:clj (alter-meta! *ns* assoc ::load false)) 19 | 20 | (defstate test-conn :start 42 21 | :stop (constantly 0)) 22 | 23 | (defstate test-nrepl :start []) 24 | 25 | (def swap-conn {:start (fn [] 42) 26 | :stop #(println "stopping test-conn-state")}) 27 | #?(:clj 28 | (def swap-nrepl {:start #(start-server :bind "localhost" :port 3442) 29 | :stop #(stop-server @nrepl)})) 30 | 31 | #?(:clj 32 | (deftest only-states 33 | 34 | (testing "only should only return given states. 35 | if source set of states is not provided, it should use all the states to select from" 36 | (is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl" "#'tapp.nyse/conn"} 37 | (only #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn})))) 38 | 39 | (testing "only should only return given states" 40 | (is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl"} 41 | (only [#'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn] 42 | #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl})))))) 43 | 44 | #?(:clj 45 | (deftest except-states 46 | 47 | (testing "except should exclude given states. 48 | if source set of states is not provided, it should use all the states to exclude from" 49 | (let [states (except #{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn})] 50 | (is (coll? states)) 51 | (is (pos? (count states))) 52 | (is (zero? (count (intersection (set states) 53 | #{"#'tapp.example/nrepl" "#'tapp.nyse/conn" "#'is.not/here"})))))) 54 | 55 | (testing "except should exclude given states" 56 | (is (= #{"#'tapp.conf/config" "#'mount.test.composable-fns/test-conn"} 57 | (set (except #{#'tapp.example/nrepl #'tapp.conf/config #'mount.test.composable-fns/test-conn} 58 | #{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn}))))))) 59 | 60 | #?(:clj 61 | (deftest states-with-args 62 | 63 | (testing "with-args should set args and return all states if none provided" 64 | (let [states (with-args {:a 42})] 65 | (is (= {:a 42} (mount/args))) 66 | (is (= states (#'mount.core/find-all-states))))) 67 | 68 | (testing "with-args should set args and thread states if provided" 69 | (let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn} 70 | states (with-args t-states {:a 42})] 71 | (is (= {:a 42} (mount/args))) 72 | (is (= states t-states)))))) 73 | 74 | #?(:clj 75 | (deftest swap-states-with-values 76 | 77 | (testing "swap should swap states with values and return all states if none is given" 78 | (let [states (swap {#'tapp.nyse/conn "conn-sub" 79 | #'tapp.example/nrepl :nrepl-sub})] 80 | (is (= states (#'mount.core/find-all-states))) 81 | (mount/start) 82 | (is (map? (dval config))) 83 | (is (= :nrepl-sub (dval nrepl))) 84 | (is (= "conn-sub" (dval conn))) 85 | (mount/stop))) 86 | 87 | (testing "swap should swap states with values and return only states that it is given" 88 | (let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn} 89 | states (swap t-states {#'tapp.nyse/conn "conn-sub" 90 | #'tapp.example/nrepl :nrepl-sub})] 91 | (is (= states t-states)) 92 | (mount/start) 93 | (is (map? (dval config))) 94 | (is (= :nrepl-sub (dval nrepl))) 95 | (is (= "conn-sub" (dval conn))) 96 | (is (= 42 (dval test-conn))) 97 | (mount/stop))))) 98 | 99 | #?(:clj 100 | (deftest swap-states-with-states 101 | 102 | (testing "swap-states should swap states with states and return all mount states if none is given" 103 | (let [states (swap-states {#'tapp.nyse/conn swap-conn 104 | #'tapp.example/nrepl swap-nrepl})] 105 | (is (= states (#'mount.core/find-all-states))) 106 | (mount/start) 107 | (is (map? (dval config))) 108 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 109 | (is (= 42 (dval conn))) 110 | (mount/stop))) 111 | 112 | (testing "swap-states should swap states on start and rollback on stop" 113 | (let [states (swap-states {#'tapp.nyse/conn swap-conn})] 114 | (is (= states (#'mount.core/find-all-states))) 115 | (mount/start #'tapp.nyse/conn) 116 | (is (= 42 (dval conn))) 117 | (mount/stop #'tapp.nyse/conn) 118 | (mount/start #'tapp.nyse/conn) 119 | (is (instance? datomic.peer.LocalConnection (dval conn))) 120 | (mount/stop))) 121 | 122 | (testing "swap-states should swap states with states and return only states that it is given" 123 | (let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.nyse/conn} 124 | states (swap-states t-states {#'tapp.nyse/conn swap-conn 125 | #'tapp.example/nrepl swap-nrepl})] 126 | (is (= states t-states)) 127 | (apply mount/start states) 128 | (is (instance? mount.core.NotStartedState (dval config))) 129 | (is (instance? mount.core.NotStartedState (dval nrepl))) 130 | (is (= 42 (dval conn))) 131 | (is (= 42 (dval test-conn))) ;; test-conn is explicitly started via "t-states" 132 | (mount/stop))))) 133 | 134 | #?(:clj 135 | (deftest composing 136 | 137 | (testing "states provided to the top level should narrow down the scope for the whole composition" 138 | (let [scope [#'tapp.conf/config 139 | #'tapp.example/nrepl 140 | #'tapp.nyse/conn 141 | #'mount.test.composable-fns/test-nrepl 142 | #'mount.test.composable-fns/test-conn] 143 | states (-> (only scope) 144 | (with-args {:a 42}) 145 | (except [#'mount.test.composable-fns/test-nrepl 146 | #'mount.test.composable-fns/test-conn]) 147 | (swap-states {#'tapp.example/nrepl swap-nrepl}) 148 | (swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}}))] 149 | (is (= #{"#'tapp.nyse/conn" "#'tapp.conf/config" "#'tapp.example/nrepl"} (set states))) 150 | (mount/start states) 151 | (is (= {:a 42} (mount/args))) 152 | (is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config))) 153 | (is (instance? datomic.peer.LocalConnection (dval conn))) 154 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 155 | (mount/stop))) 156 | 157 | (testing "should compose and start in a single composition" 158 | (let [scope [#'tapp.conf/config 159 | #'tapp.example/nrepl 160 | #'tapp.nyse/conn 161 | #'mount.test.composable-fns/test-nrepl 162 | #'mount.test.composable-fns/test-conn]] 163 | (-> (only scope) 164 | (with-args {:a 42}) 165 | (except [#'mount.test.composable-fns/test-nrepl 166 | #'mount.test.composable-fns/test-conn]) 167 | (swap-states {#'tapp.example/nrepl swap-nrepl}) 168 | (swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}}) 169 | mount/start) 170 | (is (= {:a 42} (mount/args))) 171 | (is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config))) 172 | (is (instance? datomic.peer.LocalConnection (dval conn))) 173 | (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) 174 | (mount/stop))) 175 | 176 | (testing "should not start anything on empty seq of states" 177 | (let [scope #{}] 178 | (is (= {:started #{}} (-> (only scope) 179 | mount/start))) 180 | (mount/stop))) 181 | 182 | (testing "should not stop anything on empty seq of states" 183 | (let [scope #{}] 184 | (mount/start) 185 | (is (instance? datomic.peer.LocalConnection (dval conn))) 186 | (is (= {:stopped #{}} (-> (only scope) 187 | mount/stop))) 188 | (is (instance? datomic.peer.LocalConnection (dval conn))) 189 | (mount/stop) 190 | (is (instance? mount.core.NotStartedState (dval conn))))))) 191 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /doc/differences-from-component.md: -------------------------------------------------------------------------------- 1 | ###### Differences from "Component" 2 | 3 | ## Perception 4 | 5 | Solving the "application state" in Clojure, where an application is not a tool or a library, 6 | but a product that has lots of state to deal with, is not a trivial task. 7 | The [Component](https://github.com/stuartsierra/component) framework is a solution that has been gaining popularity: 8 | 9 | > _[source](http://www.javacodegeeks.com/2015/09/clojure-web-development-state-of-the-art.html):_ 10 | 11 | > _I think all agreed that Component is the industry standard for managing lifecycle of Clojure applications. If you are a Java developer you may think of it as a Spring (DI) replacement – you declare dependencies between “components” which are resolved on “system” startup. So you just say “my component needs a repository/database pool” and component library “injects” it for you._ 12 | 13 | While this is a common understanding, the Component is far from being Spring, in a good sense: 14 | 15 | * its codebase is fairly small 16 | * it aims to solve one thing and one thing only: manage application state via inversion of control 17 | 18 | The not so hidden benefit is REPL time reloadability that it brings to the table with `component/start` and `component/stop` 19 | 20 | 21 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 22 | 23 | - [Then why "mount"!?](#then-why-mount) 24 | - [So what are the differences?](#so-what-are-the-differences) 25 | - [Component requires whole app buy in](#component-requires-whole-app-buy-in) 26 | - [Start and Stop Order](#start-and-stop-order) 27 | - [Refactoring an existing application](#refactoring-an-existing-application) 28 | - [Code navigation](#code-navigation) 29 | - [Objects vs. Namespaces](#objects-vs-namespaces) 30 | - [Starting and stopping parts of an application](#starting-and-stopping-parts-of-an-application) 31 | - [Boilerplate code](#boilerplate-code) 32 | - [Library vs. Framework](#library-vs-framework) 33 | - [What Component does better](#what-component-does-better) 34 | - [Multiple separate systems within the same JVM](#multiple-separate-systems-within-the-same-jvm) 35 | - [Visualizing dependency graph](#visualizing-dependency-graph) 36 | 37 | 38 | 39 | ## Then why "mount"!? 40 | 41 | [mount](https://github.com/tolitius/mount) was created after using Component for several projects. 42 | 43 | While Component is an interesting way to manage state, it has its limitations that prevented us 44 | from having the ultimate super power of Clojure: _fun working with it_. Plus several other disadvantages 45 | that we wanted to "fix". 46 | 47 | Before moving on to differences, [here](https://news.ycombinator.com/item?id=2467809) is a piece by Rich Hickey. While he is _not_ talking about application state, it is an interesting insight into LISP design principles: 48 | 49 | > Lisps were designed to receive a set of interactions/forms via a REPL, not to compile files/modules/programs etc. This means you can build up a Lisp program interactively in very small pieces, switching between namespaces as you go, etc. It is a very valuable part of the Lisp programming experience. It implies that you can stream fragments of Lisp programs as small as a single form over sockets, and have them be compiled and evaluated as they arrive. It implies that you can define a macro and immediately have the compiler incorporate it in the compilation of the next form, or evaluate some small section of an otherwise broken file. 50 | 51 | ## So what are the differences? 52 | 53 | ### Component requires whole app buy in 54 | 55 | Component really only works if you build your entire app around its model: application is fully based on Components 56 | where every Component is an Object. 57 | 58 | Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/wherever 59 | you need it and use it. 60 | 61 | This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "_open_" application 62 | of Namespaces and Functions to a "_closed_" application of Objects and Methods. "open" and "close" 63 | here are rather feelings, but it is way easier and more natural to 64 | 65 | * go to a namespace to see this function 66 | than to 67 | * go to a namespace, go to a component, go to another component that this function maybe using/referenced at via a component key, to get the full view of the function. 68 | 69 | Again this is mostly a personal preference: the code works in both cases. 70 | 71 | ### Start and Stop Order 72 | 73 | Component relies on a cool [dependency](https://github.com/stuartsierra/dependency) library to build 74 | a graph of dependencies, and start/stop them via topological sort based on the dependencies in this graph. 75 | 76 | Since Mount relies on Clojure namespaces and `:require`/`:use`, the order of states 77 | and their dependencies are revealed by the Clojure Compiler itself. Mount just records that order and replays 78 | it back and forth on stop and start. 79 | 80 | ### Refactoring an existing application 81 | 82 | Since to get the most benefits of Component the approach is "all or nothing", to rewrite an existing application 83 | in Component, depending on the application size, is daunting at best. 84 | 85 | Mount allows adding `defstates` _incrementally_, the same way you would add functions to an application. 86 | 87 | ### Code navigation 88 | 89 | Component changes the way the code is structured. Depending on the size of the code base, and how rich the dependency graph is, Component might add a good amount of cognitive load. To a simple navigation from namespace to namespace, from function to function, Components add, well.. "Components" that can't be ignored when [loading the codebase in one's head](http://paulgraham.com/head.html) 90 | 91 | Since Mount relies on Clojure namespaces (`:require`/`:use`), navigation across functions / states is exactly 92 | the same with or without Mount: there are no extra mental steps. 93 | 94 | ### Objects vs. Namespaces 95 | 96 | One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything. 97 | This is how Component "separates explicit dependencies" and "clears the boundaries". 98 | 99 | This is also how an Object Oriented language does it, which does not leave a lot of room for functions: 100 | with Component most of the functions are _methods_ which is an important distinction. 101 | 102 | Mount relies on Clojure namespaces to clear the boundaries. No change from Clojure here: `defstate` in one namespace 103 | can be easily `:require`d in another. 104 | 105 | ### Starting and stopping _parts_ of an application 106 | 107 | Component can't really start and stop parts of an application within the same "system". Other sub systems can be 108 | created from scratch or by dissoc'ing / merging with existing systems, but it is usually not all 109 | that flexible in terms of REPL sessions where lots of time is spent. 110 | 111 | Mount _can_ start and stop parts of an application via given states with their namespaces: 112 | 113 | ```clojure 114 | dev=> (mount/start #'app.config/app-config #'app.nyse/conn) 115 | 116 | 11:35:06.753 [nREPL-worker-1] INFO mount - >> starting.. app-config 117 | 11:35:06.756 [nREPL-worker-1] INFO mount - >> starting.. conn 118 | :started 119 | dev=> 120 | ``` 121 | 122 | Here is more [documentation](../README.md#start-and-stop-parts-of-application) on how to start/stop parts of an app. 123 | 124 | ### Boilerplate code 125 | 126 | Component does not require a whole lot of "extra" code but: 127 | 128 | * a system with dependencies 129 | * components as records 130 | * with optional constructors 131 | * and a Lifecycle/start Lifecycle/stop implementations 132 | * destructuring component maps 133 | 134 | Depending on the number of application components the "extra" size may vary. 135 | 136 | Mount is pretty much: 137 | 138 | ```clojure 139 | (defstate name :start fn 140 | :stop fn) 141 | ``` 142 | 143 | no "ceremony". 144 | 145 | ### Library vs. Framework 146 | 147 | Mount uses namespaces and vars where Component uses records and protocols. 148 | 149 | Component manages records and protocols, 150 | and in order to do that it requires a whole app buy-in, 151 | which makes it a _framework_. 152 | 153 | Mount does not need to manage namespaces and vars, 154 | since it is very well managed by the Clojure Compiler, 155 | which makes it a _library_. 156 | 157 | ## What Component does better 158 | 159 | ### Multiple separate systems within the same JVM 160 | 161 | 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_. 162 | 163 | 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. 164 | 165 | 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. 166 | 167 | Testing is not alien to Mount and it knows how to do a thing or two: 168 | 169 | * [starting / stopping parts of an application](https://github.com/tolitius/mount/blob/master/doc/differences-from-component.md#starting-and-stopping-parts-of-an-application) 170 | * [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states) 171 | * [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations) 172 | * [stop an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states) 173 | * [composing states](https://github.com/tolitius/mount#composing-states) 174 | 175 | 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). 176 | 177 | 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. 178 | 179 | ###### _conclusion: can be done with mount as well, but via a different dependency._ 180 | 181 | ### Visualizing dependency graph 182 | 183 | Component keeps an actual graph which can be visualized with great libraries like [loom](https://github.com/aysylu/loom). 184 | Having this visualization is really helpful, especially during code discusions between multiple developers. 185 | 186 | Mount does not have this at the moment. It does have all the data to create such a visualization, perhaps even 187 | by building a graph out of the data it has just for this purpose. 188 | 189 | There is a [`(states-with-deps)`](https://github.com/tolitius/mount/blob/master/src/mount/tools/graph.cljc#L20) function that can help out: 190 | 191 | ```clojure 192 | dev=> (require '[mount.tools.graph :as graph]) 193 | 194 | dev=> (graph/states-with-deps) 195 | ({:name "#'app.conf/config", 196 | :order 1, 197 | :status #{:started}, 198 | :deps #{}} 199 | {:name "#'app.db/conn", 200 | :order 2, 201 | :status #{:started}, 202 | :deps #{"#'app.conf/config"}} 203 | {:name "#'app.www/nyse-app", 204 | :order 3, 205 | :status #{:started}, 206 | :deps #{"#'app.conf/config"}} 207 | {:name "#'app.example/nrepl", 208 | :order 4, 209 | :status #{:started}, 210 | :deps #{"#'app.www/nyse-app" "#'app.conf/config"}}) 211 | ``` 212 | 213 | But it does not draw :), and it currently only supports Clojure, not ClojureScript. 214 | 215 | ###### _conclusion: needs more thinking._ 216 | -------------------------------------------------------------------------------- /src/mount/core.cljc: -------------------------------------------------------------------------------- 1 | (ns mount.core 2 | #?(:clj {:clojure.tools.namespace.repl/load false}) ; prevent reloading of this ns 3 | #?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro] 4 | [mount.tools.macrovich :refer [deftime]] 5 | [mount.tools.logger :refer [log]] 6 | [clojure.set :refer [intersection]] 7 | [clojure.string :as s]) 8 | :cljs (:require [mount.tools.macro] 9 | [clojure.set :refer [intersection]] 10 | [mount.tools.logger :refer [log]])) 11 | #?(:cljs (:require-macros [mount.core] 12 | [mount.tools.macro :refer [on-error throw-runtime]] 13 | [mount.tools.macrovich :refer [deftime]]))) 14 | 15 | (defonce ^:private -args (atom {})) ;; mostly for command line args and external files 16 | (defonce ^:private state-seq (atom 0)) 17 | (defonce ^:private mode (atom :clj)) 18 | (defonce ^:private meta-state (atom {})) 19 | (defonce ^:private running (atom {})) ;; to clean dirty states on redefs 20 | 21 | (defn- make-state-seq [state] 22 | (or (:order (@meta-state state)) 23 | (swap! state-seq inc))) 24 | 25 | (deftype NotStartedState [state] 26 | Object 27 | (toString [this] 28 | (str "'" state "' is not started (to start all the states call mount/start)"))) 29 | 30 | ;;TODO validate the whole lifecycle 31 | (defn- validate [{:keys [start stop suspend resume] :as lifecycle}] 32 | (cond 33 | (not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)") 34 | (or suspend resume) (throw-runtime "suspend / resume lifecycle support was removed in \"0.1.10\" in favor of (mount/stop-except)"))) 35 | 36 | (defn- with-ns [ns name] 37 | (str "#'" ns "/" name)) 38 | 39 | (defn- pounded? [f] 40 | (let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...))) 41 | (.startsWith (str f) pound))) 42 | 43 | (defn unpound [f] 44 | (if (pounded? f) 45 | (nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"] 46 | f)) 47 | 48 | (defn cleanup-if-dirty 49 | "in case a namespace is recompiled without calling (mount/stop), 50 | a running state instance will still be running. 51 | this function stops this 'lost' state instance. 52 | it is meant to be called by defstate before defining a new state" 53 | [state reason] 54 | (when-let [{:keys [stop] :as up} (@running state)] 55 | (when stop 56 | (log (str "<< stopping.. " state " " reason)) 57 | (stop)) 58 | (swap! running dissoc state))) 59 | 60 | #?(:clj 61 | (defn alter-state! [{:keys [var inst]} value] 62 | (if (= @mode :cljc) 63 | (reset! inst value) 64 | (alter-var-root var (constantly value)))) 65 | 66 | :cljs 67 | (defn alter-state! [{:keys [inst]} value] 68 | (reset! inst value))) 69 | 70 | (defn- update-meta! [path v] 71 | (swap! meta-state assoc-in path v)) 72 | 73 | (defn- record! [state-name f done] 74 | (let [state (f)] 75 | (swap! done conj state-name) 76 | state)) 77 | 78 | (defn- up [state {:keys [start stop status] :as current} done] 79 | (when-not (:started status) 80 | (let [s (on-error (str "could not start [" state "] due to") 81 | (record! state start done))] 82 | (alter-state! current s) 83 | (swap! running assoc state {:stop stop}) 84 | (update-meta! [state :status] #{:started})))) 85 | 86 | (defn- down 87 | "brings a state down by 88 | * calling its 'stop' function if it is defined 89 | * if not defined, state will still become a 'NotStartedState' 90 | * in case of a failure on 'stop', state is still marked as :stopped, and the error is logged / printed 91 | * dissoc'ing it from the running states 92 | * marking it as :stopped" 93 | [state {:keys [stop status] :as current} done] 94 | (when (some status #{:started}) 95 | (if stop 96 | (if-let [cause (-> (on-error (str "could not stop [" state "] due to") 97 | (record! state stop done) 98 | :fail? false) 99 | :f-failed)] 100 | (log cause :error) ;; this would mostly be useful in REPL / browser console 101 | (alter-state! current (->NotStartedState state))) 102 | (alter-state! current (->NotStartedState state))) ;; (!) if a state does not have :stop when _should_ this might leak 103 | (swap! running dissoc state) 104 | (update-meta! [state :status] #{:stopped}))) 105 | 106 | (defn running-states [] 107 | (set (keys @running))) 108 | 109 | (deftype DerefableState [name] 110 | #?(:clj clojure.lang.IDeref 111 | :cljs IDeref) 112 | (#?(:clj deref 113 | :cljs -deref) 114 | [_] 115 | (let [{:keys [status var inst] :as state} (@meta-state name)] 116 | (when-not (:started status) 117 | (if (= :throw (-> var meta :on-lazy-start)) 118 | (throw-runtime (str ":on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} " name "...) " 119 | "and " name " state was not explicitly started before it was deref'ed (i.e. @" name ")")) 120 | (up name state (atom #{})))) 121 | @inst)) 122 | #?(:clj clojure.lang.IPending 123 | :cljs IPending) 124 | (#?(:clj isRealized 125 | :cljs -realized?) 126 | [_] 127 | (boolean ((running-states) name)))) 128 | 129 | #?(:clj 130 | (defn current-state [state] 131 | (let [{:keys [var]} (@meta-state state)] 132 | (if (= @mode :cljc) 133 | (->DerefableState state) 134 | (var-get var)))) 135 | 136 | :cljs 137 | (defn current-state [state] 138 | (-> (@meta-state state) :inst deref))) 139 | 140 | (defn on-reload-meta [s-var] 141 | (or (-> s-var meta :on-reload) 142 | :restart)) ;; restart by default on ns reload 143 | 144 | (defn running-noop? [s-name] 145 | (let [{:keys [var status]} (@meta-state s-name) 146 | on-reload (-> var meta :on-reload)] 147 | (when status 148 | (and (status :started) 149 | (= :noop on-reload))))) 150 | 151 | ;;TODO: make private after figuring out the inconsistency betwen cljs compile stages 152 | ;; (i.e. _sometimes_ this, if private, is not seen by expanded "defmacro" on cljs side) 153 | (defn mount-it [s-var s-name s-meta] 154 | (let [with-inst (assoc s-meta :inst (atom (->NotStartedState s-name)) 155 | :var s-var) 156 | on-reload (on-reload-meta s-var) 157 | existing? (when-not (= :noop on-reload) 158 | (cleanup-if-dirty s-name "(namespace was recompiled)"))] 159 | (update-meta! [s-name] with-inst) 160 | (when (and existing? (= :restart on-reload)) 161 | (log (str ">> starting.. " s-name " (namespace was recompiled)")) 162 | (up s-name with-inst (atom #{}))))) 163 | 164 | (deftime 165 | 166 | (defmacro defstate 167 | "defines a state (a.k.a. a stateful component). 168 | restarts on recompilation. 169 | pass ^{:on-reload :noop} to prevent auto-restart on ns recompilation, 170 | or ^{:on-reload :stop} to stop on recompilation." 171 | [state & body] 172 | (let [[state params] (mount.tools.macro/name-with-attributes state body) 173 | {:keys [start stop] :as lifecycle} (apply hash-map params) 174 | state-name (with-ns *ns* state) 175 | order (make-state-seq state-name)] 176 | (validate lifecycle) 177 | (let [s-meta (cond-> {:order order 178 | :start `(fn [] ~start) 179 | :status #{:stopped}} 180 | stop (assoc :stop `(fn [] ~stop)))] 181 | `(do 182 | ;; (log (str "|| mounting... " ~state-name)) 183 | ;; only create/redefine a new state iff this is not a running ^{:on-reload :noop} 184 | (if-not (running-noop? ~state-name) 185 | (do 186 | (~'defonce ~state (->DerefableState ~state-name)) 187 | (mount-it (~'var ~state) ~state-name ~s-meta)) 188 | (~'defonce ~state (current-state ~state-name))) 189 | (~'var ~state))))) 190 | 191 | (defmacro defstate! [state & {:keys [start! stop!]}] 192 | (let [state-name (with-ns *ns* state)] 193 | `(defstate ~state 194 | :start (~'let [~state (mount.core/current-state ~state-name)] 195 | ~start!) 196 | :stop (~'let [~state (mount.core/current-state ~state-name)] 197 | ~stop!)))) 198 | 199 | ) 200 | 201 | (defn in-cljc-mode [] 202 | (reset! mode :cljc)) 203 | 204 | (defn in-clj-mode [] 205 | (reset! mode :clj)) 206 | 207 | ;;TODO args might need more thinking 208 | (defn args [] @-args) 209 | 210 | (defn find-all-states [] 211 | (keys @meta-state)) 212 | 213 | #?(:clj 214 | (defn- var-to-str [v] 215 | (str v))) 216 | 217 | #?(:cljs 218 | (defn var-to-str [v] 219 | (if (instance? cljs.core.Var v) 220 | (let [{:keys [ns name]} (meta v)] 221 | (with-ns ns name)) 222 | v))) 223 | 224 | (defn- unvar-state [s] 225 | (->> s (drop 2) (apply str))) ;; magic 2 is removing "#'" in state name 226 | 227 | #?(:clj 228 | (defn- was-removed? 229 | "checks if a state was removed from a namespace" 230 | [state] 231 | (-> state unvar-state symbol resolve not))) 232 | 233 | #?(:clj 234 | (defn cleanup-deleted [state] 235 | (when (was-removed? state) 236 | (cleanup-if-dirty state "(it was deleted)") 237 | (swap! meta-state dissoc state)))) 238 | 239 | (defn- bring [states fun order] 240 | (let [done (atom [])] 241 | (as-> states $ 242 | (map var-to-str $) 243 | #?(:clj ;; needs more thking in cljs, since based on sym resolve 244 | (remove cleanup-deleted $)) 245 | (select-keys @meta-state $) 246 | (sort-by (comp :order val) order $) 247 | (doseq [[k v] $] (fun k v done))) 248 | @done)) 249 | 250 | (defn- merge-lifecycles 251 | "merges with overriding _certain_ non existing keys. 252 | i.e. :stop is in a 'state', but not in a 'substitute': it should be overriden with nil 253 | however other keys of 'state' (such as :ns,:name,:order) should not be overriden" 254 | ([state sub] 255 | (merge-lifecycles state nil sub)) 256 | ([state origin {:keys [start stop status]}] 257 | (assoc state :origin origin 258 | :status status 259 | :start start :stop stop))) 260 | 261 | (defn- rollback! [state] 262 | (let [{:keys [origin] :as sub} (@meta-state state)] 263 | (when origin 264 | (update-meta! [state] (merge-lifecycles sub origin))))) 265 | 266 | (defn- substitute! [state with mode] 267 | (let [lifecycle-fns #(select-keys % [:start :stop :status]) 268 | origin (@meta-state state) 269 | sub (if (= :value mode) 270 | {:start (fn [] with) :status :stopped} 271 | (assoc with :status :stopped))] 272 | (update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub)))) 273 | 274 | (defn- unsub [state] 275 | (when (-> (@meta-state state) :sub?) 276 | (update-meta! [state :sub?] nil))) 277 | 278 | (defn- all-without-subs [] 279 | (remove (comp :sub? @meta-state) (find-all-states))) 280 | 281 | (defn start [& states] 282 | (let [fs (-> states first)] 283 | (if (coll? fs) 284 | (if-not (empty? fs) ;; (mount/start) vs. (mount/start #{}) vs. (mount/start #{1 2 3}) 285 | (apply start fs) 286 | {:started #{}}) 287 | (let [states (or (->> states (map var-to-str) seq) 288 | (all-without-subs))] 289 | {:started (bring states up <)})))) 290 | 291 | (defn stop [& states] 292 | (let [fs (-> states first)] 293 | (if (coll? fs) 294 | (if-not (empty? fs) ;; (mount/stop) vs. (mount/stop #{}) vs. (mount/stop #{1 2 3}) 295 | (apply stop fs) 296 | {:stopped #{}}) 297 | (let [states (or (->> states (map var-to-str) seq) 298 | (find-all-states)) 299 | _ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with" / "swap-states" 300 | stopped (bring states down >)] 301 | (dorun (map rollback! states)) ;; restore to origin from "start-with" / "swap-states" 302 | {:stopped stopped})))) 303 | 304 | ;; composable set of states 305 | 306 | (defn- mapset [f xs] 307 | (-> (map f xs) 308 | set)) 309 | 310 | (defn only 311 | ([these] 312 | (only (find-all-states) these)) 313 | ([states these] 314 | (intersection (mapset var-to-str these) 315 | (mapset var-to-str states)))) 316 | 317 | (defn with-args 318 | ([args] 319 | (with-args (find-all-states) args)) 320 | ([states args] 321 | (reset! -args args) ;; TODO localize 322 | states)) 323 | 324 | (defn except 325 | ([these] 326 | (except (find-all-states) these)) 327 | ([states these] 328 | (remove (mapset var-to-str these) 329 | (mapset var-to-str states)))) 330 | 331 | (defn swap 332 | ([with] 333 | (swap (find-all-states) with)) 334 | ([states with] 335 | (doseq [[from to] with] 336 | (substitute! (var-to-str from) 337 | to :value)) 338 | states)) 339 | 340 | (defn swap-states 341 | ([with] 342 | (swap-states (find-all-states) with)) 343 | ([states with] 344 | (doseq [[from to] with] 345 | (substitute! (var-to-str from) 346 | to :state)) 347 | states)) 348 | 349 | ;; restart on events 350 | 351 | (defprotocol ChangeListener 352 | (add-watcher [this ks watcher]) 353 | (on-change [this k])) 354 | 355 | (deftype RestartListener [watchers] 356 | ChangeListener 357 | 358 | (add-watcher [_ ks state] 359 | (doseq [k ks] 360 | (swap! watchers update k (fn [v] 361 | (-> (conj v state) vec))))) 362 | 363 | (on-change [_ ks] 364 | (doseq [k ks] 365 | (when-let [states (seq (@watchers k))] 366 | (apply stop states) 367 | (apply start states))))) 368 | 369 | (defn restart-listener 370 | ([] 371 | (restart-listener {})) 372 | ([watchers] 373 | (RestartListener. (atom watchers)))) 374 | 375 | ;; explicit, not composable (subject to depreciate?) 376 | 377 | (defn stop-except [& states] 378 | (let [all (set (find-all-states)) 379 | states (map var-to-str states) 380 | states (remove (set states) all)] 381 | (if-not (empty? states) 382 | (apply stop states) 383 | {:stopped #{}}))) 384 | 385 | (defn start-with-args [xs & states] 386 | (reset! -args xs) 387 | (if (first states) 388 | (apply start states) 389 | (start))) 390 | 391 | (defn start-with [with] 392 | (doseq [[from to] with] 393 | (substitute! (var-to-str from) 394 | to :value)) 395 | (start)) 396 | 397 | (defn start-with-states [with] 398 | (doseq [[from to] with] 399 | (substitute! (var-to-str from) 400 | to :state)) 401 | (start)) 402 | 403 | (defn start-without [& states] 404 | (if (first states) 405 | (let [app (set (all-without-subs)) 406 | states (map var-to-str states) 407 | without (remove (set states) app)] 408 | (if-not (empty? without) 409 | (apply start without) 410 | {:started #{}})) 411 | (start))) 412 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > I think that it's _extraordinarily important_ that we in computer science keep fun in computing 2 | 3 | _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](https://web.mit.edu/6.001/6.037/sicp.pdf)_ 4 | 5 | # mount 6 | [![ (or just [open an issue](https://github.com/tolitius/mount/issues)) 10 | 11 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 12 | 13 | - [Why?](#why) 14 | - [Differences from Component](#differences-from-component) 15 | - [How](#how) 16 | - [Creating State](#creating-state) 17 | - [Using State](#using-state) 18 | - [Dependencies](#dependencies) 19 | - [Talking States](#talking-states) 20 | - [Value of Values](#value-of-values) 21 | - [The Importance of Being Reloadable](#the-importance-of-being-reloadable) 22 | - [Start and Stop Order](#start-and-stop-order) 23 | - [Composing States](#composing-states) 24 | - [Start and Stop Parts of Application](#start-and-stop-parts-of-application) 25 | - [Start an Application Without Certain States](#start-an-application-without-certain-states) 26 | - [Swapping Alternate Implementations](#swapping-alternate-implementations) 27 | - [Swapping States with Values](#swapping-states-with-values) 28 | - [Swapping States with States](#swapping-states-with-states) 29 | - [Stop an Application Except Certain States](#stop-an-application-except-certain-states) 30 | - [ClojureScript is Clojure](doc/clojurescript.md#managing-state-in-clojurescript) 31 | - [cljc mode](#cljc-mode) 32 | - [Disable Lazy Start](#disable-lazy-start) 33 | - [Packaging](#packaging) 34 | - [Affected States](#affected-states) 35 | - [Recompiling Namespaces with Running States](#recompiling-namespaces-with-running-states) 36 | - [:on-reload](#on-reload) 37 | - [Cleaning up Deleted States](#cleaning-up-deleted-states) 38 | - [Logging](#logging) 39 | - [mount-up](#mount-up) 40 | - [Manual AOP](#manual-aop) 41 | - [Exception Handling](#exception-handling) 42 | - [Clojure Version](#clojure-version) 43 | - [Mount and Develop!](#mount-and-develop) 44 | - [Running New York Stock Exchange](#running-new-york-stock-exchange) 45 | - [New York Stock Exchange Maintenance](#new-york-stock-exchange-maintenance) 46 | - [Web and Uberjar](#web-and-uberjar) 47 | - [Runtime Arguments](#runtime-arguments) 48 | - [License](#license) 49 | 50 | 51 | 52 | ## Why? 53 | 54 | Clojure is 55 | 56 | * powerful 57 | * simple 58 | * and _fun_ 59 | 60 | Depending on how application state is managed during development, the above three superpowers can either stay, 61 | go somewhat, or go completely. 62 | 63 | If Clojure REPL (i.e. `lein repl`, `boot repl`) fired up instantly, the need to reload application state 64 | inside the REPL would go away. But at the moment, and for some time in the future, managing state by making it 65 | reloadable within the same REPL session is important to retain all the Clojure superpowers. 66 | 67 | Here is a good [breakdown](http://blog.ndk.io/clojure-bootstrapping.html) on the Clojure REPL 68 | startup time, and it is [not because of JVM](http://blog.ndk.io/jvm-slow-startup.html). 69 | 70 | `mount` is here to preserve all the Clojure superpowers while making _the application state_ enjoyably reloadable. 71 | 72 | There is another Clojure superpower that `mount` is made to retain: Clojure community. 73 | Pull request away, let's solve this thing! 74 | 75 | ### Differences from Component 76 | 77 | `mount` is an alternative to the [component](https://github.com/stuartsierra/component) approach with notable [differences](doc/differences-from-component.md#differences-from-component). 78 | 79 | ## How 80 | 81 | ```clojure 82 | (require '[mount.core :refer [defstate]]) 83 | ``` 84 | 85 | ### Creating State 86 | 87 | Creating state is easy: 88 | 89 | ```clojure 90 | (defstate conn :start (create-conn)) 91 | ``` 92 | 93 | where the `create-conn` function creates a connection (for example to a database) and is defined elsewhere, can be right above it. 94 | 95 | In case this state needs to be cleaned / destroyed between reloads, there is also `:stop` 96 | 97 | ```clojure 98 | (defstate conn :start (create-conn) 99 | :stop (disconnect conn)) 100 | ``` 101 | 102 | That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply 103 | `required` by other namespaces or in REPL: 104 | 105 | ```clojure 106 | dev=> (require '[app.db :refer [conn]]) 107 | nil 108 | dev=> conn 109 | #object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"] 110 | ``` 111 | 112 | ### Using State 113 | 114 | For example let's say an `app` needs a connection above. No problem: 115 | 116 | ```clojure 117 | (ns app 118 | (:require [above :refer [conn]])) 119 | ``` 120 | 121 | where `above` is an arbitrary namespace that defines the above state / connection. 122 | 123 | ### Documentation String 124 | 125 | As in any definition (i.e. `def`, `defn`) a documentation string can be added to better describe a state: 126 | 127 | ```clojure 128 | (defstate answer 129 | "answer to the ultimate question of life universe and everything" 130 | :start (+ 1 41)) 131 | ``` 132 | ```clojure 133 | (doc answer) 134 | ------------------------- 135 | dev/answer 136 | answer to the ultimate question of life universe and everything 137 | ``` 138 | 139 | ## Dependencies 140 | 141 | If the whole app is one big application context (or `system`), cross dependencies with a solid dependency graph 142 | is an integral part of the system. 143 | 144 | But if a state is a simple top level being, these beings can coexist with each other and with other 145 | namespaces by being `required` instead. 146 | 147 | If a managing state library requires a whole app buy-in, where everything is a bean or a component, 148 | it is a framework, and dependency graph is usually quite large and complex, 149 | since it has _everything_ (every piece of the application) in it. 150 | 151 | But if stateful things are kept lean and low level (i.e. I/O, queues, threads, connections, etc.), dependency graphs are simple and small, and everything else is just namespaces and functions: the way it should be. 152 | 153 | ### Talking States 154 | 155 | There are of course direct dependencies that `mount` respects: 156 | 157 | ```clojure 158 | (ns app.config 159 | (:require [mount.core :refer [defstate]])) 160 | 161 | (defstate config 162 | :start (load-config "test/resources/config.edn")) 163 | ``` 164 | 165 | this `config`, being top level, can be used in other namespaces, including the ones that create states: 166 | 167 | ```clojure 168 | (ns app.database 169 | (:require [mount.core :refer [defstate]] 170 | [app.config :refer [config]])) 171 | 172 | (defstate conn :start (create-connection config)) 173 | ``` 174 | 175 | [here](dev/clj/app/www.clj#L32) is an example of a web server that "depends" on a similar `config`. 176 | 177 | ###### _(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)_ 178 | 179 | ## Value of values 180 | 181 | Lifecycle functions start/stop can take both functions and values. This is "valuable" and also works: 182 | 183 | ```clojure 184 | (defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42) 185 | ``` 186 | 187 | While it would be useful in REPL and for testing, real application states would usually have start / stop logic, in other words, the real lifecycle. 188 | 189 | Besides scalar values, lifecycle functions can take anonymous functions, partial functions, function references, etc.. Here are some examples: 190 | 191 | ```clojure 192 | (defn f [n] 193 | (fn [m] 194 | (+ n m))) 195 | 196 | (defn g [a b] 197 | (+ a b)) 198 | 199 | (defn- pf [n] 200 | (+ 41 n)) 201 | 202 | (defn fna [] 203 | 42) 204 | 205 | (defstate scalar :start 42) 206 | (defstate fun :start #(inc 41)) 207 | (defstate with-fun :start (inc 41)) 208 | (defstate with-partial :start (partial g 41)) 209 | (defstate f-in-f :start (f 41)) 210 | (defstate f-no-args-value :start (fna)) 211 | (defstate f-no-args :start fna) 212 | (defstate f-args :start g) 213 | (defstate f-value :start (g 41 1)) 214 | (defstate private-f :start pf) 215 | ``` 216 | 217 | Check out [fun-with-values-test](test/core/mount/test/fun_with_values.cljc) for more details. 218 | 219 | ## The Importance of Being Reloadable 220 | 221 | `mount` has start and stop functions that will walk all the states created with `defstate` and start / stop them 222 | accordingly: i.e. will call their `:start` and `:stop` defined functions. Hence the whole application state can be reloaded in REPL e.g.: 223 | 224 | ``` 225 | dev=> (require '[mount.core :as mount]) 226 | 227 | dev=> (mount/stop) 228 | dev=> (mount/start) 229 | ``` 230 | 231 | While it is not always necessary, mount lifecycle can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace), 232 | to make the whole application reloadable with refreshing the app namespaces. 233 | Here is a [dev.clj](dev/clj/dev.clj) as an example, that sums up to: 234 | 235 | ```clojure 236 | (defn go [] 237 | (start) 238 | :ready) 239 | 240 | (defn reset [] 241 | (stop) 242 | (tn/refresh :after 'dev/go)) 243 | ``` 244 | 245 | the `(reset)` is then used in REPL to restart / reload application state without the need to restart the REPL itself. 246 | 247 | ## Start and Stop Order 248 | 249 | Since dependencies are "injected" by `require`ing on the namespace level, `mount` **trusts the Clojure compiler** to 250 | maintain the start and stop order for all the `defstates`. 251 | 252 | The "start" order is then recorded and replayed on each `(reset)`. 253 | 254 | The "stop" order is simply `(reverse "start order")`: 255 | 256 | ```clojure 257 | dev=> (reset) 258 | 08:21:39.430 [nREPL-worker-1] DEBUG mount - << stopping.. nrepl 259 | 08:21:39.431 [nREPL-worker-1] DEBUG mount - << stopping.. conn 260 | 08:21:39.432 [nREPL-worker-1] DEBUG mount - << stopping.. config 261 | 262 | :reloading (app.config app.nyse app.utils.datomic app) 263 | 264 | 08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting.. config 265 | 08:21:39.463 [nREPL-worker-1] DEBUG mount - >> starting.. conn 266 | 08:21:39.481 [nREPL-worker-1] DEBUG mount - >> starting.. nrepl 267 | :ready 268 | ``` 269 | 270 | You can see examples of start and stop flows in the [example app](README.md#mount-and-develop). 271 | 272 | ## Composing States 273 | 274 | Besides calling `(mount/start)` there are other useful ways to start an application: 275 | 276 | * [starting parts of an application](README.md#start-and-stop-parts-of-application) 277 | * [starting an application without certain states](README.md#start-an-application-without-certain-states) 278 | * [swapping alternate implementations](README.md#swapping-alternate-implementations) 279 | * [passing runtime arguments](README.md#runtime-arguments) 280 | 281 | 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_. 282 | 283 | ### Composer's Toolbox 284 | 285 | Each "tool" has a single responsibility and can be composed with other tools in _any_ combination and order. 286 | 287 | * `only` will return _only_ states that it is given + exist (seen by mount) in the application 288 | * `except` will return all the states that it is given _except_ a given set 289 | * `swap` will take a map with keys as states and values as their substitute values 290 | * `swap-states` will take a map with keys as states and values with `{:start fn :stop fn}` as their substitute states 291 | * `with-args` will take a map that could later be accessed by `(mount/args)` 292 | 293 | 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. 294 | 295 | 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)`. 296 | 297 | ### Be Composing 298 | 299 | All of the above is much easier to understand by looking at examples: 300 | 301 | ```clojure 302 | (-> (only #{#'foo/a 303 | #'foo/b 304 | #'foo/c 305 | #'bar/d 306 | #'baz/e}) 307 | (except [#'foo/c 308 | #'bar/d]) 309 | (with-args {:a 42}) 310 | mount/start) 311 | ``` 312 | 313 | 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`. 314 | 315 | 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. 316 | 317 | Here is a more "involved" example: 318 | 319 | ```clojure 320 | (-> (only #{#'foo/a 321 | #'foo/b 322 | #'foo/c 323 | #'bar/d 324 | #'baz/e}) 325 | (with-args {:a 42}) 326 | (except [#'foo/c 327 | #'bar/d]) 328 | (swap-states {#'foo/a {:start #(create-connection test-conf) 329 | :stop #(disconnect a)}}) 330 | (swap {#'baz/e {:datomic {:uri "datomic:mem://composable-mount"}}}) 331 | mount/start) 332 | ``` 333 | 334 | 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. 335 | 336 | ## Start and Stop Parts of Application 337 | 338 | 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". 339 | 340 | `mount`'s lifecycle functions, i.e. start/stop, can _optionally_ take states as vars (i.e. prefixed with their namespaces): 341 | 342 | ```clojure 343 | (mount/start #'app.config/config #'app.nyse/conn) 344 | ... 345 | (mount/stop #'app.config/config #'app.nyse/conn) 346 | ``` 347 | 348 | which will _only_ start/stop `config` and `conn` (won't start/stop any other states). 349 | 350 | Here is an [example](test/core/mount/test/parts.cljc) test that uses only two namespaces checking that the third one is not started. 351 | 352 | ## Start an Application Without Certain States 353 | 354 | Whether it is in REPL or during testing, it is often useful to start an application _without_ certain states. These can be queue listeners that are not needed at REPL time, or a subset of an application to test. 355 | 356 | The `start-without` function can do just that: 357 | 358 | ```clojure 359 | (mount/start-without #'app.feeds/feed-listener 360 | #'app/nrepl) 361 | ``` 362 | 363 | which will start an application without starting `feed-listener` and `nrepl` states. 364 | 365 | Here is an [example](test/core/mount/test/start_without.cljc) test that excludes Datomic connection and nREPL from an application on start. 366 | 367 | ## Swapping Alternate Implementations 368 | 369 | During testing it is often very useful to mock/stub certain states. For example running a test against an in memory database vs. the real one, running with a publisher that publishes to a test core.async channel vs. the real remote queue, etc. 370 | 371 | ### Swapping States with Values 372 | 373 | The `start-with` function takes values as substitutes. 374 | 375 | Say we have a `send-sms` state: 376 | 377 | ```clojure 378 | (ns app.sms) 379 | ;; ... 380 | (defstate send-sms :start (create-sms-sender 381 | (:sms config))) 382 | ``` 383 | 384 | When running tests it would be great _not_ to send the real text messages, but rather send them all to a local core.async channel instead: 385 | 386 | ```clojure 387 | (let [sms-ch (chan) 388 | send-sms (fn [sms] (go (>! sms-ch sms)))] 389 | (mount/start-with {#'app.sms/send-sms send-sms}) ;; <<<< swapping the "send-sms" state with a test function 390 | ;; testing.. checking "sms-ch" channel 391 | (mount/stop)) 392 | ``` 393 | 394 | `start-with` takes a map of states with their substitutes. For example `#'app.sms/send-sms` here is the real deal SMS sender that is being substituted with a `send-sms` test function. 395 | 396 | ### Swapping States with States 397 | 398 | The `start-with-states` function takes values in a form of `{:start fn :stop fn}` as substitutes: 399 | 400 | ```clojure 401 | (mount/start-with-states {#'app.neo/db {:start #(connect test-config) 402 | :stop #(disconnect db)} 403 | #'app.neo/publisher {:start #(create-pub test-config) 404 | :stop #(close-pub publisher)}}) 405 | ``` 406 | 407 | `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 408 | substituted with `#(connect test-config)` function, which could end up being anything, a map, an in memory DB, etc. 409 | 410 | The `:stop` functions of substitutes can be anything, and could refer to the original state references. As in the example above: `db` and `publisher` 411 | are real references. They would need to be accessible from the namespace of course, so you might need to `(:require [app.neo :refer [db]])` 412 | in order to use `db` in `:stop #(disconnect db)` example above. 413 | 414 | -- 415 | 416 | One thing to note is whenever 417 | 418 | ```clojure 419 | (mount/stop) 420 | ``` 421 | 422 | is run after `start-with`/`start-with-states`, it rolls back to an original "state of states", i.e. `#'app.neo/db` is `#'app.neo/db` again. So subsequent calls to `(mount/start)` or even to `(mount/start-with {something else})` will start from a clean slate. 423 | 424 | Here is an [example](test/core/mount/test/start_with_states.cljc) test that starts an app with mocking Datomic connection and nREPL. 425 | 426 | ## Stop an Application Except Certain States 427 | 428 | Calling `(mount/stop)` will stop all the application states. In case everything needs to be stopped _besides certain ones_, it can be done with `(mount/stop-except)`. 429 | 430 | Here is an example of restarting the application without bringing down `#'app.www/nyse-app`: 431 | 432 | ```clojure 433 | dev=> (mount/start) 434 | 14:34:10.813 [nREPL-worker-0] INFO mount.core - >> starting.. config 435 | 14:34:10.814 [nREPL-worker-0] INFO mount.core - >> starting.. conn 436 | 14:34:10.814 [nREPL-worker-0] INFO app.db - creating a connection to datomic: datomic:mem://mount 437 | 14:34:10.838 [nREPL-worker-0] INFO mount.core - >> starting.. nyse-app 438 | 14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:4242 439 | 14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED org.eclipse.jetty.server.Server@194f37af 440 | 14:34:10.844 [nREPL-worker-0] INFO mount.core - >> starting.. nrepl 441 | :started 442 | 443 | dev=> (mount/stop-except #'app.www/nyse-app) 444 | 14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. nrepl 445 | 14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. conn 446 | 14:34:47.766 [nREPL-worker-0] INFO app.db - disconnecting from datomic:mem://mount 447 | 14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. config 448 | :stopped 449 | dev=> 450 | 451 | dev=> (mount/start) 452 | 14:34:58.673 [nREPL-worker-0] INFO mount.core - >> starting.. config 453 | 14:34:58.674 [nREPL-worker-0] INFO app.config - loading config from test/resources/config.edn 454 | 14:34:58.674 [nREPL-worker-0] INFO mount.core - >> starting.. conn 455 | 14:34:58.674 [nREPL-worker-0] INFO app.db - creating a connection to datomic: datomic:mem://mount 456 | 14:34:58.693 [nREPL-worker-0] INFO mount.core - >> starting.. nrepl 457 | :started 458 | ``` 459 | 460 | 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. 461 | 462 | ## Recompiling Namespaces with Running States 463 | 464 | Mount will detect when a namespace with states (i.e. with `(defstate ...)`) was reloaded/recompiled, 465 | and will check every state in this namespace whether it was running at the point of recompilation. If it was, _it will restart it_: 466 | 467 | * if a state has a `:stop` function, mount will invoke it on the old version of state (i.e. cleanup) 468 | * it will call a "new" `:start` function _after_ this state is recompiled/redefined 469 | 470 | Mount won't keep it a secret, it'll tell you about all the states that had to be restarted during namespace reload/recompilation: 471 | 472 | 473 | 474 | same is true for recompiling and reloading (figwheel, boot-reload, etc.) namespaces in ClojureScript: 475 | 476 | 477 | 478 | Providing a `:stop` function _is_ optional, but in case a state needs to be cleaned between restarts or on a system shutdown, 479 | `:stop` is highly recommended. 480 | 481 | ### :on-reload 482 | 483 | 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". 484 | 485 | This behavior could be controlled with an optional `:on-reload` meta attribute when defining a state. 486 | 487 | In case _nothing_ needs to be done to a running state on reload / recompile / redef, set `:on-reload` to `:noop`: 488 | 489 | ```clojure 490 | (defstate ^{:on-reload :noop} 491 | mem-db :start (connect config) 492 | :stop (disconnect mem-db)) 493 | ``` 494 | 495 | When a running state needs to be just "stopped" on reload, set `:on-reload` to `:stop`: 496 | 497 | ```clojure 498 | (defstate ^{:on-reload :stop} 499 | mem-db :start (connect config) 500 | :stop (disconnect mem-db)) 501 | ``` 502 | 503 | 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. 504 | 505 | 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. 506 | 507 | ## Cleaning up Deleted States 508 | 509 | Mount will detect when a state was renamed/deleted from a namespace, and will do two things: 510 | 511 | * if a state had a `:stop` function, mount will invoke it on the old version of state (i.e. cleanup) 512 | * will remove any knowledge of this state internally 513 | 514 | Here is an example: 515 | 516 | ```clojure 517 | dev=> (defstate won't-be-here-long :start (println "I am starting... ") 518 | :stop (println "I am stopping... ")) 519 | #'dev/won't-be-here-long 520 | dev=> 521 | 522 | dev=> (mount/start #'dev/won't-be-here-long) 523 | INFO app.utils.logging - >> starting.. #'dev/won't-be-here-long 524 | I am starting... 525 | {:started ["#'dev/won't-be-here-long"]} 526 | dev=> 527 | ``` 528 | 529 | "deleting" it from REPL, and starting all the states: 530 | 531 | ```clojure 532 | dev=> (ns-unmap 'dev 'won't-be-here-long) 533 | nil 534 | dev=> (mount/start) 535 | 536 | "<< stopping.. #'dev/won't-be-here-long (it was deleted)" 537 | I am stopping... 538 | 539 | INFO app.utils.logging - >> starting.. #'app.conf/config 540 | INFO app.utils.logging - >> starting.. #'app.db/conn 541 | INFO app.utils.logging - >> starting.. #'app.www/nyse-app 542 | INFO app.utils.logging - >> starting.. #'app.example/nrepl 543 | {:started ["#'app.conf/config" "#'app.db/conn" "#'app.www/nyse-app" "#'app.example/nrepl"]} 544 | ``` 545 | 546 | Mount detected that `#'dev/won't-be-here-long` was deleted, hence: 547 | ```clojure 548 | << stopping.. #'dev/won't-be-here-long (it was deleted) 549 | ``` 550 | 551 | ## `cljc` mode 552 | 553 | 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. 554 | 555 | 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)`. 556 | 557 | ### Disable Lazy Start 558 | 559 | 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: 560 | 561 | ```clojure 562 | => (mount/in-cljc-mode) 563 | :cljc 564 | 565 | => (defstate db-connection :start (println "connecting") 566 | :stop (println "disconnecting...")) 567 | 568 | => db-connection 569 | #object[mount.core.DerefableState 0x546b9d51 {:status :pending, :val nil}] 570 | 571 | dev=> (mount/running-states) 572 | #{} 573 | 574 | dev=> @db-connection ;; db-connection will start here when deref'ed even though it was not started explicitly 575 | connecting 576 | 577 | dev=> (mount/running-states) 578 | #{"#'dev/db-connection"} 579 | ``` 580 | 581 | This can be quite handy as it allows certain app states to start lazily. 582 | 583 | 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: 584 | 585 | ```clojure 586 | => (defstate ^{:on-lazy-start :throw} db-connection :start (do (println "connecting") 42) 587 | :stop (println "disconnecting...")) 588 | 589 | => @db-connection ;; this will throw since db connection is deref'ed before it was started 590 | 591 | 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) 592 | 593 | => (mount/start #'dev/db-connection) 594 | connecting 595 | {:started ["#'dev/db-connection"]} 596 | 597 | => @db-connection 598 | 42 599 | ``` 600 | 601 | ## Packaging 602 | 603 | 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. 604 | 605 | At the development time this requirement is mostly transparent, since these namespaces are compiled with nREPL, or refreshed with "tools.namespace", etc. But it becomes important when _packaging_ an application or when starting a web application via [lein-ring](https://github.com/weavejester/lein-ring#general-options)'s or [boot-http](https://github.com/pandeiro/boot-http#-i----init-and--c----cleanup)'s `:init` hooks. 606 | 607 | Depending on a structure and a kind of an application, this means that these namespaces need to be _`:required`_ prior to a call to `mount/start` when packaging the app as a stand alone JAR or a WAR. 608 | 609 | This can be easily done with choosing an application entry point, which could be a web handler namespace with routes or just an arbitrary app namespace (i.e. `my.app`). In this app entry point namespace all other namespaces that have `defstate` would be `:require`d and a call to the `mount/start` function would be defined: 610 | 611 | ```clojure 612 | (ns my.app 613 | (:require [a] 614 | [b] 615 | [c] 616 | [mount.core :as mount])) 617 | 618 | (defn rock-n-roll [] ;; or (defn -main [args].. ) 619 | (mount/start)) 620 | ``` 621 | 622 | this would ensure that at the time `(rock-n-roll)` is called, all the namespaces with states were compiled (i.e. mount knows about them). `(rock-n-roll)` can be used in/as a -main function or as a web hook such as `:init`. 623 | 624 | In practice only a few namespaces need to be `:require`d, since others will be brought in transitively (i.e. by already required namespaces). From the `my.app` example above, say we had namespaces `d`, `e` and `f` that are required by `a`, and `g` and `h` that are required by `b`. They (`d`, `e`, `f`, `g` and `h`) _won't_ need to be required by `my.app`, since `a` and `b` would "bring" them in. 625 | 626 | ## Affected States 627 | 628 | Every time a lifecycle function (start/stop) is called mount will return all the states that were affected: 629 | 630 | ```clojure 631 | dev=> (mount/start) 632 | {:started [#'app.config/config 633 | #'app.nyse/conn 634 | #'app/nrepl]} 635 | ``` 636 | ```clojure 637 | dev=> (mount/stop) 638 | {:stopped [#'app/nrepl 639 | #'app.nyse/conn 640 | #'app.config/config]} 641 | ``` 642 | 643 | An interesting bit here is a vector vs. a set: all the states are returned _in the order they were affected_. 644 | 645 | ## Logging 646 | 647 | > All the mount examples have `>> starting..` / `<< stopping..` logging messages, but when I develop an application with mount I don't see them. 648 | 649 | Valid question. It was a [conscious choice](https://github.com/tolitius/mount/issues/15) not to depend on any particular logging library, since there are few to select from, and this decision is best left to the developer who may choose to use mount. 650 | 651 | Since mount is a _library_ it should _not_ bring any dependencies unless its functionality directly depends on them. 652 | 653 | > _But I still these logging statements in the examples..._ 654 | 655 | ### mount-up 656 | 657 | One way to do that would be using "[mount-up](https://github.com/tolitius/mount-up)" that "watches mount's ups and downs": 658 | 659 | ```clojure 660 | => (require '[mount-up.core :as mu]) 661 | 662 | => (mu/on-upndown :info mu/log :before) 663 | 664 | => (mount/start) 665 | INFO mount-up.core - >> starting.. #'boot.user/server 666 | {:started ["#'boot.user/server"]} 667 | 668 | => (mount/stop) 669 | INFO mount-up.core - << stopping.. #'boot.user/server 670 | {:stopped ["#'boot.user/server"]} 671 | ``` 672 | 673 | ### Manual AOP 674 | 675 | 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). 676 | 677 | ## Exception Handling 678 | 679 | One way to handle exceptions on start/stop would be to simply wrap start/stop functions in `try/catch`. 680 | 681 | Another way would be to use a custom [mount-up](https://github.com/tolitius/mount-up/blob/master/README.md#wrapping) wrapper. 682 | 683 | ## Clojure Version 684 | 685 | 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. 686 | 687 | ## Mount and Develop! 688 | 689 | Besides a [a collection](https://github.com/tolitius/stater) of sample mount applications, mount _sources_ come with two sample apps: 690 | 691 | * Clojure [app](dev/clj/app) 692 | * ClojureScript [app](doc/clojurescript.md#mounting-that-clojurescript) 693 | 694 | You can clone mount, jump into a REPL and start playing with these built in apps. 695 | 696 | Below is an example of the Clojure app that comes with mount. 697 | 698 | The app has 4 states: 699 | 700 | * `config`, loaded from the files and refreshed on each `(reset)` 701 | * `datomic connection` that uses the config to create itself 702 | * `nyse web app` which is a web server with compojure routes (i.e. the actual app) 703 | * `nrepl` that uses config to bind to host/port 704 | 705 | ### Running New York Stock Exchange 706 | 707 | To try it out, clone `mount`, get to REPL (`boot repl` or `lein repl`) and switch to `(dev)`: 708 | 709 | ```clojure 710 | $ boot repl 711 | 712 | user=> (dev) 713 | #object[clojure.lang.Namespace 0xcf1a0cc "dev"] 714 | ``` 715 | 716 | start/restart/reset everything using `(reset)`: 717 | 718 | ```clojure 719 | dev=> (reset) 720 | 721 | :reloading (mount.tools.macro mount.core app.utils.logging app.conf app.db app.utils.datomic app.nyse app.www app.example dev) 722 | INFO app.utils.logging - >> starting.. #'app.conf/config 723 | INFO app.conf - loading config from dev/resources/config.edn 724 | INFO app.utils.logging - >> starting.. #'app.db/conn 725 | INFO app.db - conf: {:datomic {:uri datomic:mem://mount}, :www {:port 4242}, :h2 {:classname org.h2.Driver, :subprotocol h2, :subname jdbc:h2:mem:mount, :user sa, :password }, :rabbit {:api-port 15672, :password guest, :queue r-queue, :username guest, :port 5672, :node jabit, :exchange-type direct, :host 192.168.1.1, :vhost /captoman, :auto-delete-q? true, :routing-key , :exchange foo}, :nrepl {:host 0.0.0.0, :port 7878}} 726 | INFO app.db - creating a connection to datomic: datomic:mem://mount 727 | INFO app.utils.logging - >> starting.. #'app.www/nyse-app 728 | INFO app.utils.logging - >> starting.. #'app.example/nrepl 729 | dev=> 730 | ``` 731 | 732 | everything is started and can be played with: 733 | 734 | ```clojure 735 | dev=> (add-order conn {:ticker "GOOG" :bid 665.51M :offer 665.59M :qty 100}) 736 | dev=> (add-order conn {:ticker "GOOG" :bid 665.50M :offer 665.58M :qty 300}) 737 | 738 | dev=> (find-orders conn "GOOG") 739 | ({:db/id 17592186045418, :order/symbol "GOOG", :order/bid 665.51M, :order/qty 100, :order/offer 665.59M} 740 | {:db/id 17592186045420, :order/symbol "GOOG", :order/bid 665.50M, :order/qty 300, :order/offer 665.58M}) 741 | ``` 742 | 743 | since there is also a web server running, we can add orders with HTTP POST (from a different terminal window): 744 | 745 | ```clojure 746 | $ curl -X POST -d "ticker=TSLA&qty=100&bid=232.38&offer=232.43" "http://localhost:4242/nyse/orders" 747 | 748 | {"added":{"ticker":"TSLA","qty":"100","bid":"232.38","offer":"232.43"}} 749 | ``` 750 | 751 | ```clojure 752 | dev=> (find-orders conn "TSLA") 753 | ({:db/id 17592186045422, :order/symbol "TSLA", :order/bid 232.38M, :order/qty 100, :order/offer 232.43M}) 754 | ``` 755 | 756 | once something is changed in the code, or you just need to reload everything, do `(reset)`. 757 | 758 | _note: a simple `(mount/stop)` / `(mount/start)` will also work, `(reset)` is for "convenience + ns refresh":_ 759 | 760 | ```clojure 761 | dev=> (reset) 762 | INFO app.utils.logging - << stopping.. #'app.example/nrepl 763 | INFO app.utils.logging - << stopping.. #'app.www/nyse-app 764 | INFO app.utils.logging - << stopping.. #'app.db/conn 765 | INFO app.db - disconnecting from datomic:mem://mount 766 | INFO app.utils.logging - << stopping.. #'app.conf/config 767 | 768 | :reloading (app.conf app.db app.nyse app.www app.example dev) 769 | 770 | INFO app.utils.logging - >> starting.. #'app.conf/config 771 | INFO app.conf - loading config from dev/resources/config.edn 772 | INFO app.utils.logging - >> starting.. #'app.db/conn 773 | INFO app.db - conf: {:datomic {:uri datomic:mem://mount}, :www {:port 4242}, :h2 {:classname org.h2.Driver, :subprotocol h2, :subname jdbc:h2:mem:mount, :user sa, :password }, :rabbit {:api-port 15672, :password guest, :queue r-queue, :username guest, :port 5672, :node jabit, :exchange-type direct, :host 192.168.1.1, :vhost /captoman, :auto-delete-q? true, :routing-key , :exchange foo}, :nrepl {:host 0.0.0.0, :port 7878}} 774 | INFO app.db - creating a connection to datomic: datomic:mem://mount 775 | INFO app.utils.logging - >> starting.. #'app.www/nyse-app 776 | INFO app.utils.logging - >> starting.. #'app.example/nrepl 777 | :ready 778 | ``` 779 | 780 | notice that it stopped and started again. 781 | 782 | In `app.db` connection `:stop` calls a `disconnect` function where a [database is deleted](https://github.com/tolitius/mount/blob/e3066fe024f89420bd4463a433c5d3b893b7b315/dev/clj/app/db.clj#L18). Hence after `(reset)` was called the app was brought its starting point: [database was created](https://github.com/tolitius/mount/blob/e3066fe024f89420bd4463a433c5d3b893b7b315/dev/clj/app/db.clj#L11) by the 783 | `:start` that calls a `new-connection` function, and db schema is [created](https://github.com/tolitius/mount/blob/e3066fe024f89420bd4463a433c5d3b893b7b315/dev/clj/app/www.clj#L26) by `nyse.app`. 784 | 785 | But again no orders: 786 | 787 | ```clojure 788 | dev=> (find-orders conn "GOOG") 789 | () 790 | dev=> (find-orders conn "TSLA") 791 | () 792 | ``` 793 | 794 | hence the app is in its "clean" state, and ready to rock and roll as right after the REPL started: 795 | 796 | ```clojure 797 | dev=> (add-order conn {:ticker "TSLA" :bid 232.381M :offer 232.436M :qty 250}) 798 | 799 | dev=> (find-orders conn "TSLA") 800 | ({:db/id 17592186045418, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M}) 801 | ``` 802 | 803 | ### New York Stock Exchange Maintenance 804 | 805 | 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: 806 | 807 | ```clojure 808 | dev=> (mount/stop #'app.www/nyse-app) 809 | INFO app.utils.logging - << stopping.. #'app.www/nyse-app 810 | {:stopped ["#'app.www/nyse-app"]} 811 | dev=> 812 | ``` 813 | ```bash 814 | $ curl localhost:4242 815 | curl: (7) Failed to connect to localhost port 4242: Connection refused 816 | ``` 817 | 818 | everything but the web server works as before: 819 | 820 | ```clojure 821 | dev=> (find-orders conn "TSLA") 822 | ({:db/id 17592186045420, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M}) 823 | dev=> 824 | ``` 825 | 826 | once we found who `DDoS`ed us on `:4242`, and punished them, we can restart the web server: 827 | 828 | ```clojure 829 | dev=> (mount/start #'app.www/nyse-app) 830 | INFO app.utils.logging - >> starting.. #'app.www/nyse-app 831 | {:started ["#'app.www/nyse-app"]} 832 | dev=> 833 | ``` 834 | 835 | ```clojure 836 | $ curl localhost:4242 837 | welcome to the mount sample app! 838 | ``` 839 | 840 | ## Web and Uberjar 841 | 842 | There is an `uberjar` branch with an example webapp and it's uberjar sibling. Before trying it: 843 | 844 | ```clojure 845 | $ git checkout uberjar 846 | Switched to branch 'uberjar' 847 | ``` 848 | 849 | The documentation is [here](doc/uberjar.md#creating-reloadable-uberjarable-app). 850 | 851 | ## Runtime Arguments 852 | 853 | There is an `with-args` branch with an example app that takes command line params 854 | 855 | ```clojure 856 | $ git checkout with-args 857 | Switched to branch 'with-args' 858 | ``` 859 | 860 | The documentation is [here](doc/runtime-arguments.md#passing-runtime-arguments). 861 | 862 | ## License 863 | 864 | Copyright © 2020 tolitius 865 | 866 | Distributed under the Eclipse Public License either version 1.0 or (at 867 | your option) any later version. 868 | --------------------------------------------------------------------------------