├── 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 |
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 | [)
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 |
--------------------------------------------------------------------------------