Compare commits
430 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcdff7577a | ||
|
|
984979b347 | ||
|
|
2f4d1a3581 | ||
|
|
2bca9089b8 | ||
|
|
656af38802 | ||
|
|
bfd7adf98f | ||
|
|
08173bc6a1 | ||
|
|
3fc6c58cd6 | ||
|
|
ccaf99910c | ||
|
|
9c588f68c2 | ||
|
|
f16d7004ee | ||
|
|
3caea10121 | ||
|
|
fb09fef496 | ||
|
|
b89b930ec2 | ||
|
|
4065f5df36 | ||
|
|
632977ff41 | ||
|
|
5275023b38 | ||
|
|
ad6ca6fb5e | ||
|
|
eff4c04f21 | ||
|
|
5df6c941f7 | ||
|
|
098b56d14f | ||
|
|
58e2ded430 | ||
|
|
8a3fc385a2 | ||
|
|
2d050e9055 | ||
|
|
b77f504cfd | ||
|
|
5ac52b725b | ||
|
|
c85da6149c | ||
|
|
6cf5390a44 | ||
|
|
bc3924aedf | ||
|
|
5d992042e4 | ||
|
|
83542e56a7 | ||
|
|
232df7a8a3 | ||
|
|
10dbbaa40b | ||
|
|
5b26fb6092 | ||
|
|
e9f9dfca14 | ||
|
|
348297ee11 | ||
|
|
52831f6b3e | ||
|
|
9666d3f372 | ||
|
|
5564e367e7 | ||
|
|
f65ed6a266 | ||
|
|
6dafae195d | ||
|
|
5fab543501 | ||
|
|
2ba3c60995 | ||
|
|
fb52f79396 | ||
|
|
c5f3e4cdf8 | ||
|
|
76e9a71a13 | ||
|
|
8de6b09989 | ||
|
|
884a2b2d87 | ||
|
|
c2687d1b9f | ||
|
|
17112646f1 | ||
|
|
d9c4f621cd | ||
|
|
ed1c9944f6 | ||
|
|
e07d7d6aad | ||
|
|
686b79e03f | ||
|
|
2ff032c8b3 | ||
|
|
244d834c46 | ||
|
|
0ff5fe106c | ||
|
|
bb23747273 | ||
|
|
2d5566ae86 | ||
|
|
1acd4feb4c | ||
|
|
c210cc486c | ||
|
|
4f8384763b | ||
|
|
6e848d1ee4 | ||
|
|
db38d8cacd | ||
|
|
594cc58b71 | ||
|
|
e956dd4de5 | ||
|
|
0a208bd275 | ||
|
|
5e0a4c5bfc | ||
|
|
562340f4dc | ||
|
|
b3f3f49d64 | ||
|
|
a4552fc6ed | ||
|
|
697775ef45 | ||
|
|
75d05e3158 | ||
|
|
dc59f3ec98 | ||
|
|
1ccff026de | ||
|
|
be6e9be246 | ||
|
|
52e360f3ab | ||
|
|
fdb85e0fe2 | ||
|
|
e247636be6 | ||
|
|
e032a6ef54 | ||
|
|
dc7dbdd51e | ||
|
|
677812e3ee | ||
|
|
ac16b04b68 | ||
|
|
14100ad648 | ||
|
|
8bb5e19303 | ||
|
|
fb97606b37 | ||
|
|
a23def2e2f | ||
|
|
e9e107f4fa | ||
|
|
31e0b68918 | ||
|
|
4d961b56c0 | ||
|
|
095eb81488 | ||
|
|
6ae0255677 | ||
|
|
355b9b3ea3 | ||
|
|
2f51e5049e | ||
|
|
f01b0538e1 | ||
|
|
448375d2dc | ||
|
|
a49a7e4d55 | ||
|
|
b873590f0a | ||
|
|
2cface9878 | ||
|
|
2f4ffa47a7 | ||
|
|
436514d407 | ||
|
|
404ea70bfb | ||
|
|
891869721c | ||
|
|
ce5f5ed90a | ||
|
|
94d6d0188a | ||
|
|
9a62688c2f | ||
|
|
736eacdf33 | ||
|
|
17461b1911 | ||
|
|
8f8df230a9 | ||
|
|
7fbb433e2f | ||
|
|
c9945aef5b | ||
|
|
f38bc2f0e7 | ||
|
|
49591d4368 | ||
|
|
62cb9dd516 | ||
|
|
e2ef7bba55 | ||
|
|
087683968d | ||
|
|
f49fceb518 | ||
|
|
5ec4017fdb | ||
|
|
c56bbfcd40 | ||
|
|
a397ae5cc5 | ||
|
|
1c7f2ccc34 | ||
|
|
3218f8afaf | ||
|
|
a0f10407ee | ||
|
|
c189db9d12 | ||
|
|
7d323e727e | ||
|
|
d9ea3a56f0 | ||
|
|
9b063504e8 | ||
|
|
e9ba02b5a5 | ||
|
|
ae6763da8c | ||
|
|
fea36ec2a5 | ||
|
|
0cc94ad1d2 | ||
|
|
d9fd79001d | ||
|
|
8f10b85718 | ||
|
|
1f18cb7067 | ||
|
|
147d9c52fb | ||
|
|
09ccb3fb1d | ||
|
|
f514782716 | ||
|
|
3d767f6256 | ||
|
|
76fb498968 | ||
|
|
e2c73fdc19 | ||
|
|
110f9ee666 | ||
|
|
76cae93f81 | ||
|
|
ccfde46678 | ||
|
|
6ee07a9901 | ||
|
|
307331a4f2 | ||
|
|
35655ed752 | ||
|
|
19bed168c8 | ||
|
|
690f9fc6c8 | ||
|
|
736d93abd1 | ||
|
|
0acb78e16a | ||
|
|
82ec3d34aa | ||
|
|
7ee66db407 | ||
|
|
f4683252df | ||
|
|
b7a1c070bb | ||
|
|
5755b7cc49 | ||
|
|
31f92fd52c | ||
|
|
677aa0cfdc | ||
|
|
e34a164c2e | ||
|
|
afe96e9aad | ||
|
|
f26da26d80 | ||
|
|
69a592cec6 | ||
|
|
7becc38282 | ||
|
|
8920b36597 | ||
|
|
bd2ba3ac6d | ||
|
|
c3ce0c6401 | ||
|
|
dc089c1aec | ||
|
|
78b9a23c71 | ||
|
|
9131cbb17a | ||
|
|
ef4dee6c8a | ||
|
|
c001c19eb5 | ||
|
|
f6e6a92055 | ||
|
|
064c00599c | ||
|
|
8026a7b69c | ||
|
|
d5eb56c422 | ||
|
|
a1e8e9c8e0 | ||
|
|
f801599f68 | ||
|
|
926e4a9d2a | ||
|
|
c2828cc3dc | ||
|
|
44d57734b7 | ||
|
|
977806626d | ||
|
|
b77bef3457 | ||
|
|
98496d63d8 | ||
|
|
dc91a44f72 | ||
|
|
4787ae12f5 | ||
|
|
4a59fab9ba | ||
|
|
346a5b7f8c | ||
|
|
71663ba87c | ||
|
|
46b4895d95 | ||
|
|
c9de434aa0 | ||
|
|
6f64640ada | ||
|
|
d8749261df | ||
|
|
92736ba2d0 | ||
|
|
64596b2674 | ||
|
|
9694b775aa | ||
|
|
be4f6d9d56 | ||
|
|
c10a65dee2 | ||
|
|
39a01c4727 | ||
|
|
dc5c89b3e9 | ||
|
|
8a3ce619e8 | ||
|
|
b4a79f7ba3 | ||
|
|
1646511179 | ||
|
|
0a746f00ad | ||
|
|
1d7322ef98 | ||
|
|
c918a11e3a | ||
|
|
3875215dc3 | ||
|
|
e3066fe024 | ||
|
|
d834c6a2b0 | ||
|
|
07eef10a77 | ||
|
|
5e57e3ae7e | ||
|
|
afb1dd772a | ||
|
|
8ecfbf613b | ||
|
|
c72613a609 | ||
|
|
c023a3491c | ||
|
|
6115d7a813 | ||
|
|
ac46e9c0f9 | ||
|
|
6fb67f333b | ||
|
|
d1eab49ad7 | ||
|
|
62cff2d0e9 | ||
|
|
b24e7a17fe | ||
|
|
3632ee9601 | ||
|
|
38f1dabdf2 | ||
|
|
0644b02703 | ||
|
|
627d0d5844 | ||
|
|
7fe4541e90 | ||
|
|
600701d38a | ||
|
|
9bfd642c9b | ||
|
|
9024ce6427 | ||
|
|
07b6a5e970 | ||
|
|
18a96c6cda | ||
|
|
4a0f4eaf10 | ||
|
|
1e05c3942f | ||
|
|
3b39c3b017 | ||
|
|
d2b14d0cb5 | ||
|
|
0bb137d53f | ||
|
|
a896197316 | ||
|
|
ef8227d3ba | ||
|
|
4661534411 | ||
|
|
e8ab0b81e1 | ||
|
|
5b6df4454c | ||
|
|
f9f14ee8e8 | ||
|
|
a0d312fbd4 | ||
|
|
64a91625aa | ||
|
|
8cedfaa60a | ||
|
|
0622b64941 | ||
|
|
fb12c30245 | ||
|
|
e1fa733a05 | ||
|
|
93439ed41a | ||
|
|
7495a9bdeb | ||
|
|
559ba28987 | ||
|
|
92cab30519 | ||
|
|
23b0f70927 | ||
|
|
4f218f4feb | ||
|
|
242080bf2d | ||
|
|
6e1e449d27 | ||
|
|
76225dd4ca | ||
|
|
f250941b12 | ||
|
|
6591a4731c | ||
|
|
fc7b61c406 | ||
|
|
1c6bd9c12f | ||
|
|
1ea1d28477 | ||
|
|
9740d23eb7 | ||
|
|
f98df5b2c4 | ||
|
|
a75c69c3f4 | ||
|
|
c19ef8af9a | ||
|
|
490954decf | ||
|
|
59663a87a9 | ||
|
|
7eed08db9b | ||
|
|
2b6af3ba9b | ||
|
|
2d25fd9b58 | ||
|
|
4e5e4fba71 | ||
|
|
bb45c14db5 | ||
|
|
1e1afa94fd | ||
|
|
5035552e75 | ||
|
|
a8d46cbd3a | ||
|
|
0c0a519a6b | ||
|
|
a9eb10c414 | ||
|
|
58ca345896 | ||
|
|
a4dfe74c66 | ||
|
|
f936726b34 | ||
|
|
d66832aaf1 | ||
|
|
7055cddccd | ||
|
|
f8ac28157f | ||
|
|
32604432bc | ||
|
|
a0bc17073e | ||
|
|
9d23f190b3 | ||
|
|
c7a737126f | ||
|
|
c7bb4d6a26 | ||
|
|
a75b087b8f | ||
|
|
8f9f44e3cc | ||
|
|
d67f318ef1 | ||
|
|
ca45d22111 | ||
|
|
5aac6198d0 | ||
|
|
5478d4298b | ||
|
|
a6a08b3ea0 | ||
|
|
002a45fa5e | ||
|
|
4fb6f6849e | ||
|
|
eeda080322 | ||
|
|
c1be3c377e | ||
|
|
dc5b6c2614 | ||
|
|
d342b43f23 | ||
|
|
7e2adce991 | ||
|
|
05f7c35712 | ||
|
|
40cbf173ee | ||
|
|
85e1db6f23 | ||
|
|
52c1b53510 | ||
|
|
1ac28981a6 | ||
|
|
5a01b59560 | ||
|
|
5d8c068e4d | ||
|
|
37d64d0a25 | ||
|
|
e36d524176 | ||
|
|
c7435cd81a | ||
|
|
0825ad2ed0 | ||
|
|
2211491e91 | ||
|
|
9972893710 | ||
|
|
3fc53f747e | ||
|
|
4b6a4b1658 | ||
|
|
7271ba4f42 | ||
|
|
a8ff02c348 | ||
|
|
c32f7a892f | ||
|
|
f049f4fad6 | ||
|
|
5fdef469b7 | ||
|
|
f06dc427f0 | ||
|
|
268162324f | ||
|
|
8e34001122 | ||
|
|
61f54cf12a | ||
|
|
dd6175a917 | ||
|
|
8a57f8590f | ||
|
|
d1d14bc710 | ||
|
|
03f13d1b71 | ||
|
|
7669aa2786 | ||
|
|
87e59dedcc | ||
|
|
362036a3b9 | ||
|
|
da0922850a | ||
|
|
2dc27e8589 | ||
|
|
57b63ddcaa | ||
|
|
b07b29f028 | ||
|
|
2697494268 | ||
|
|
3d95aa9fc6 | ||
|
|
fb45dbbb4e | ||
|
|
e8b04318e3 | ||
|
|
f1f423b822 | ||
|
|
020e8dfd61 | ||
|
|
e4c10cf209 | ||
|
|
ef735d7ccb | ||
|
|
fd4d846c3f | ||
|
|
7f720403ea | ||
|
|
bf5294edb7 | ||
|
|
31c1501b0a | ||
|
|
f404bb4420 | ||
|
|
958d7e345c | ||
|
|
325eecf0c9 | ||
|
|
f2f4c1f3ea | ||
|
|
20e230b5dd | ||
|
|
db510832be | ||
|
|
0e4e507343 | ||
|
|
ead12a7e88 | ||
|
|
c22c4d5c92 | ||
|
|
8e5926ac75 | ||
|
|
6d2a8d3eec | ||
|
|
8d10a2a7db | ||
|
|
339d79e508 | ||
|
|
865c4f278b | ||
|
|
1c970e152a | ||
|
|
9be822b2e9 | ||
|
|
2e5d2f90d0 | ||
|
|
7ded6ffab5 | ||
|
|
bf14f125d2 | ||
|
|
3179ed2233 | ||
|
|
e278ce9f39 | ||
|
|
7c9ccb2f3f | ||
|
|
3b4b094d20 | ||
|
|
bbf06c46b1 | ||
|
|
357069bef1 | ||
|
|
f2eb7dc22f | ||
|
|
19ec27eb9b | ||
|
|
432496863e | ||
|
|
2d1f77df92 | ||
|
|
41d4c130b3 | ||
|
|
f967e8e11a | ||
|
|
2e0279aef7 | ||
|
|
ab4737af6e | ||
|
|
ca4375c482 | ||
|
|
5db4c853db | ||
|
|
3e9b10b439 | ||
|
|
eec80639e7 | ||
|
|
79f153d08c | ||
|
|
86895bcb03 | ||
|
|
4e97280be9 | ||
|
|
ed2b837555 | ||
|
|
45861c8581 | ||
|
|
fac6c04d0d | ||
|
|
7212b60197 | ||
|
|
992f67397a | ||
|
|
ae9c343820 | ||
|
|
a05a73f82c | ||
|
|
e381ef7485 | ||
|
|
d3044c81c5 | ||
|
|
2d5df6dabe | ||
|
|
8bfa207e26 | ||
|
|
8616fb4d80 | ||
|
|
d7ed8ec71b | ||
|
|
ab0b611978 | ||
|
|
10a7625742 | ||
|
|
c0791c6f4a | ||
|
|
45934f0780 | ||
|
|
a7770968db | ||
|
|
d0cc35e988 | ||
|
|
09e7c78d22 | ||
|
|
46d2f98c73 | ||
|
|
4ec7bb5e57 | ||
|
|
9ee066ed96 | ||
|
|
0e178e4a24 | ||
|
|
f9aa4e463f | ||
|
|
5ae24e0c05 | ||
|
|
14f86dceac | ||
|
|
143e6f082b | ||
|
|
2d8730b51a | ||
|
|
26ff0939db | ||
|
|
5976331c57 | ||
|
|
beef424715 | ||
|
|
50055e7b8b | ||
|
|
cdebd056a2 | ||
|
|
52725ce918 | ||
|
|
ffb4f2a166 | ||
|
|
17012b6c90 | ||
|
|
2e08fce2f4 | ||
|
|
d019ee7b80 | ||
|
|
2e9c77b85b | ||
|
|
44aca144df | ||
|
|
34bdb4dbe7 |
1
.clj-kondo/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
{:config-paths ["../resources/clj-kondo.exports/mount/mount/"]}
|
||||
16
.gitignore
vendored
|
|
@ -1,8 +1,16 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
.cpcache/
|
||||
.rebel_readline_history
|
||||
cljs-test-runner-out
|
||||
node_modules
|
||||
pom.xml.asc
|
||||
.repl*
|
||||
dev/resources/public/js/*
|
||||
figwheel_server.log
|
||||
build.xml
|
||||
doo-index.html
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
|
|
@ -10,5 +18,7 @@ pom.xml.asc
|
|||
*.iml
|
||||
/.idea
|
||||
/.lein-repl-history
|
||||
|
||||
|
||||
/.nrepl-history
|
||||
.cljs_rhino_repl/
|
||||
out/
|
||||
.clj-kondo/.cache
|
||||
|
|
|
|||
26
.travis.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
sudo: false
|
||||
language: java
|
||||
script:
|
||||
- boot test
|
||||
- boot test-cljs
|
||||
- boot test-cljs-advanced
|
||||
install:
|
||||
- mkdir -p ~/bin
|
||||
- export PATH=~/bin:$PATH
|
||||
|
||||
- curl -L https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh -o ~/bin/boot
|
||||
- chmod +x ~/bin/boot
|
||||
jdk: openjdk8
|
||||
env:
|
||||
matrix:
|
||||
- BOOT_CLOJURE_VERSION=1.8.0
|
||||
global:
|
||||
- JAVA_OPTS="-Xms512m -Xmx2048m"
|
||||
jdk:
|
||||
- openjdk8
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
- $HOME/.boot/cache/bin
|
||||
- $HOME/.boot/cache/lib
|
||||
- $HOME/bin
|
||||
135
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
## 0.1.19
|
||||
###### Wed Aug 21 22:43:40 2024 -0400
|
||||
|
||||
* add `clojure-kondo`
|
||||
|
||||
## 0.1.17
|
||||
###### Mon Dec 19 15:17:21 2022 -0500
|
||||
|
||||
* 2d050e9 fix: swap-states non string rollback (thanks to [egg-juxt](https://github.com/egg-juxt))
|
||||
|
||||
## 0.1.16
|
||||
###### Mon Jan 28 10:34:26 2019 -0500
|
||||
|
||||
* fb52f79 prevent reloading of mount.core ns ([106](https://github.com/tolitius/mount/issues/106))
|
||||
* c5f3e4c current-state should return Derefable on :cljc ([104](https://github.com/tolitius/mount/issues/104))
|
||||
* c2687d1 silent `*logger*` warning in latest ClojureScript([101](https://github.com/tolitius/mount/issues/101))
|
||||
* bb23747 switch form circle to travis
|
||||
|
||||
## 0.1.14
|
||||
###### Thu Oct 25 18:34:22 2018 -0400
|
||||
|
||||
* cljs: throw js/Error not just a string ([#100](https://github.com/tolitius/mount/issues/100))
|
||||
* add ^{:on-lazy-start :throw} ([#95](https://github.com/tolitius/mount/issues/95) and [#99](https://github.com/tolitius/mount/issues/99))
|
||||
* self hosted ClojureScript support ([#85](https://github.com/tolitius/mount/issues/85) and [#97](https://github.com/tolitius/mount/issues/97))
|
||||
|
||||
## 0.1.12
|
||||
###### Sat Feb 10 14:53:04 2018 -0500
|
||||
|
||||
* remove ref to old state alias in `swap-states` ([#89](https://github.com/tolitius/mount/issues/89))
|
||||
* `:on-reload :noop` should not leave stale references
|
||||
* `stop-except` & `start-without` to take all states ([#81](https://github.com/tolitius/mount/issues/81))
|
||||
* goog.log/error for logging cljs errors ([#63](https://github.com/tolitius/mount/issues/63))
|
||||
* on failure (down) reports and cleans up vs. just "throw" ([#63](https://github.com/tolitius/mount/issues/63))
|
||||
* do not start a `DerefableState` as a side-effect of printing ([#76](https://github.com/tolitius/mount/issues/76))
|
||||
|
||||
## 0.1.11
|
||||
###### Fri Dec 2 18:09:51 2016 -0600
|
||||
|
||||
* opening `(find-all-states)`
|
||||
* stop accepts a collection of states (in addition to varargs) ([#69](https://github.com/tolitius/mount/issues/69))
|
||||
* swap-states and start-with-states take values ([#68](https://github.com/tolitius/mount/issues/68))
|
||||
* adding `(running-states)` that returns a set
|
||||
* `(mount/start #{})` is a noop ([#65](https://github.com/tolitius/mount/issues/65))
|
||||
* removing log on state discovery
|
||||
* adding a restart listener with watchers
|
||||
|
||||
## 0.1.10
|
||||
###### Sun Feb 28 18:20:52 2016 -0500
|
||||
|
||||
* runtime args are now initialized to `{}` (rather than to `:no-args`)
|
||||
* composing states on mount start ([#47](https://github.com/tolitius/mount/issues/47))
|
||||
* removing `:suspend` and `:resume` ([#46](https://github.com/tolitius/mount/issues/46))
|
||||
|
||||
## 0.1.9
|
||||
###### Sun Jan 31 15:47:19 2016 -0500
|
||||
|
||||
* `:on-reload` #{:noop :stop :restart} ([#36](https://github.com/tolitius/mount/issues/36))
|
||||
* swapping states with values ([#45](https://github.com/tolitius/mount/issues/45))
|
||||
* `(mount.core/system)` experiment is refactored to [Yurt](https://github.com/tolitius/yurt)
|
||||
* cleaning up deleted states ([#42](https://github.com/tolitius/mount/issues/42))
|
||||
* refactoring mount sample app (4 states, stateless fns)
|
||||
* refactoring cljs logging to Closure (goog.log)
|
||||
|
||||
## 0.1.8
|
||||
###### Mon Jan 4 14:09:17 2016 -0500
|
||||
|
||||
* pluging in [boot-check](https://github.com/tolitius/boot-check)
|
||||
* refactoring reader conditionals out of cljs exceptions macro (thx [@DomKM](https://github.com/DomKM))
|
||||
* riding on [boot-stripper](https://github.com/tolitius/boot-stripper)
|
||||
* mount.tools.graph: [states-with deps](https://github.com/tolitius/mount/blob/0.1.8/src/mount/tools/graph.cljc#L20)
|
||||
* fixing bug in start-with-args (thx [@malchmih](https://github.com/malchmih)) ([#30](https://github.com/tolitius/mount/issues/30))
|
||||
* 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))
|
||||
* restarting a state on ns recompile ([#22](https://github.com/tolitius/mount/issues/22))
|
||||
|
||||
## 0.1.7
|
||||
###### Mon Dec 21 20:52:31 2015 -0500
|
||||
|
||||
* making mount [boot](https://github.com/boot-clj/boot)'iful
|
||||
* cljs `:classifier "aot"` is fixed by boot ([#23](https://github.com/tolitius/mount/issues/23))
|
||||
* refactoring example app: + www
|
||||
* stopping/cleaning state when its namespace is recompiled ([#22](https://github.com/tolitius/mount/issues/22))
|
||||
|
||||
## 0.1.6
|
||||
###### Thu Dec 10 00:40:18 2015 -0500
|
||||
|
||||
* adding full ClojureScript support ([#10](https://github.com/tolitius/mount/issues/10))
|
||||
* removing all the dependencies (`:dependencies []`)
|
||||
* adding a sample [cljs app](https://github.com/tolitius/mount/blob/1ac28981a6a63a103a9057fd34a338c37acb913b/doc/clojurescript.md#mounting-that-clojurescript) (datascript, websockets)
|
||||
* introducting `cljc` and `clj` [modes](https://github.com/tolitius/mount/blob/1ac28981a6a63a103a9057fd34a338c37acb913b/doc/clojurescript.md#mount-modes)
|
||||
* `DerefableState`: states are _optionally_ derefable (via `IDeref`)
|
||||
* removing dependency on var's meta
|
||||
|
||||
## 0.1.5
|
||||
###### Tue Dec 1 08:58:26 2015 -0500
|
||||
|
||||
* cleaning up stale states ([#18](https://github.com/tolitius/mount/issues/18))
|
||||
* adding ns to state order to avoid collisions
|
||||
* consolidating status ([#19](https://github.com/tolitius/mount/issues/19))
|
||||
* lifecycle fns take fns and values ([#20](https://github.com/tolitius/mount/issues/20))
|
||||
* not retaining heads in side-effectful iterations ([#17](https://github.com/tolitius/mount/issues/17))
|
||||
* logging AOP for REPL examples ([#15](https://github.com/tolitius/mount/issues/15))
|
||||
* lifecycle functions return states touched ([#15](https://github.com/tolitius/mount/issues/15))
|
||||
* removing tools.logging dep ([#15](https://github.com/tolitius/mount/issues/15))
|
||||
* removing tools.macro dep
|
||||
* removing tools.namespace dep
|
||||
|
||||
## 0.1.4
|
||||
###### Sat Nov 21 15:31:13 2015 -0500
|
||||
|
||||
* [suspendable states](https://github.com/tolitius/mount#suspending-and-resuming)
|
||||
* [stop-except](https://github.com/tolitius/mount#stop-an-application-except-certain-states)
|
||||
|
||||
## 0.1.3
|
||||
###### Wed Nov 18 00:43:44 2015 -0500
|
||||
|
||||
* states-with-deps [[#12](https://github.com/tolitius/mount/issues/12)]
|
||||
* mount => mount.core [[#11](https://github.com/tolitius/mount/issues/11)]
|
||||
* states without `:stop` still become `NotStartedState` on `(mount/stop)`
|
||||
|
||||
## 0.1.2
|
||||
###### Sun Nov 15 23:15:20 2015 -0500
|
||||
|
||||
* [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations)
|
||||
* [start-without](https://github.com/tolitius/mount#start-an-application-without-certain-states)
|
||||
|
||||
## 0.1.1
|
||||
###### Sat Nov 14 16:40:38 2015 -0500
|
||||
|
||||
* [support for runtime arguments](https://github.com/tolitius/mount#runtime-arguments)
|
||||
|
||||
## 0.1.0
|
||||
###### Fri Nov 13 17:00:43 2015 -0500
|
||||
|
||||
* defstate/start/stop
|
||||
* welcome mount
|
||||
31
Makefile
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.PHONY: clean test jar tag outdated install deploy tree repl
|
||||
|
||||
clean:
|
||||
rm -rf target
|
||||
rm -rf classes
|
||||
|
||||
jar: clean test tag
|
||||
clojure -A:jar
|
||||
|
||||
test: clean
|
||||
clojure -X:test :patterns '[".*"]' # clojure tests
|
||||
# clojure -Atest-cljs -r ".*test.self.host.*" # clojure script tests
|
||||
# run "j8; boot test-cljs" until running cljs tests via deps.edn is fixed
|
||||
|
||||
outdated:
|
||||
clojure -M:outdated
|
||||
|
||||
tag:
|
||||
clojure -A:tag
|
||||
|
||||
install: jar
|
||||
clojure -A:install
|
||||
|
||||
deploy: jar
|
||||
clojure -A:deploy
|
||||
|
||||
tree:
|
||||
mvn dependency:tree
|
||||
|
||||
repl:
|
||||
clojure -A:dev -A:repl
|
||||
717
README.md
|
|
@ -1,13 +1,13 @@
|
|||
> I think that it's _extraordinarily important_ that we in computer science keep fun in computing
|
||||
|
||||
_**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](https://mitpress.mit.edu/sicp/full-text/book/book-Z-H-3.html)_
|
||||
_**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](https://web.mit.edu/6.001/6.037/sicp.pdf)_
|
||||
|
||||
# mount
|
||||
# mount <img src="doc/img/mount-logo.png" width="70px">
|
||||
[](https://github.com/tolitius/mount/releases)
|
||||
[](https://clojars.org/mount)
|
||||
|
||||

|
||||
###### _any_ questions or feedback: [`#mount`](https://clojurians.slack.com/messages/mount/) clojurians slack channel <img src="doc/img/slack-icon.png" width="15px"> (or just [open an issue](https://github.com/tolitius/mount/issues))
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
|
||||
|
||||
- [Why?](#why)
|
||||
|
|
@ -17,11 +17,34 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
|
|||
- [Using State](#using-state)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Talking States](#talking-states)
|
||||
- [Value of Values](#value-of-values)
|
||||
- [The Importance of Being Reloadable](#the-importance-of-being-reloadable)
|
||||
- [Start and Stop Order](#start-and-stop-order)
|
||||
- [Composing States](#composing-states)
|
||||
- [Start and Stop Parts of Application](#start-and-stop-parts-of-application)
|
||||
- [Start an Application Without Certain States](#start-an-application-without-certain-states)
|
||||
- [Swapping Alternate Implementations](#swapping-alternate-implementations)
|
||||
- [Swapping States with Values](#swapping-states-with-values)
|
||||
- [Swapping States with States](#swapping-states-with-states)
|
||||
- [Stop an Application Except Certain States](#stop-an-application-except-certain-states)
|
||||
- [ClojureScript is Clojure](doc/clojurescript.md#managing-state-in-clojurescript)
|
||||
- [cljc mode](#cljc-mode)
|
||||
- [Disable Lazy Start](#disable-lazy-start)
|
||||
- [Packaging](#packaging)
|
||||
- [Affected States](#affected-states)
|
||||
- [Recompiling Namespaces with Running States](#recompiling-namespaces-with-running-states)
|
||||
- [:on-reload](#on-reload)
|
||||
- [Cleaning up Deleted States](#cleaning-up-deleted-states)
|
||||
- [Logging](#logging)
|
||||
- [mount-up](#mount-up)
|
||||
- [Manual AOP](#manual-aop)
|
||||
- [Exception Handling](#exception-handling)
|
||||
- [Clojure Version](#clojure-version)
|
||||
- [Mount and Develop!](#mount-and-develop)
|
||||
- [Running New York Stock Exchange](#running-new-york-stock-exchange)
|
||||
- [New York Stock Exchange Maintenance](#new-york-stock-exchange-maintenance)
|
||||
- [Web and Uberjar](#web-and-uberjar)
|
||||
- [Runtime Arguments](#runtime-arguments)
|
||||
- [License](#license)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
|
@ -41,8 +64,8 @@ If Clojure REPL (i.e. `lein repl`, `boot repl`) fired up instantly, the need to
|
|||
inside the REPL would go away. But at the moment, and for some time in the future, managing state by making it
|
||||
reloadable within the same REPL session is important to retain all the Clojure superpowers.
|
||||
|
||||
Here is a good [breakdown](http://blog.ndk.io/2014/02/25/clojure-bootstrapping.html) on the Clojure REPL
|
||||
startup time, and it is [not because of JVM](http://blog.ndk.io/2014/02/11/jvm-slow-startup.html).
|
||||
Here is a good [breakdown](http://blog.ndk.io/clojure-bootstrapping.html) on the Clojure REPL
|
||||
startup time, and it is [not because of JVM](http://blog.ndk.io/jvm-slow-startup.html).
|
||||
|
||||
`mount` is here to preserve all the Clojure superpowers while making _the application state_ enjoyably reloadable.
|
||||
|
||||
|
|
@ -51,12 +74,12 @@ Pull request away, let's solve this thing!
|
|||
|
||||
### Differences from Component
|
||||
|
||||
mount is an alternative to the [component](https://github.com/stuartsierra/component) approach with notable [differences](doc/differences-from-component.md#differences-from-component).
|
||||
`mount` is an alternative to the [component](https://github.com/stuartsierra/component) approach with notable [differences](doc/differences-from-component.md#differences-from-component).
|
||||
|
||||
## How
|
||||
|
||||
```clojure
|
||||
(require '[mount :refer [defstate]])
|
||||
(require '[mount.core :refer [defstate]])
|
||||
```
|
||||
|
||||
### Creating State
|
||||
|
|
@ -67,9 +90,9 @@ Creating state is easy:
|
|||
(defstate conn :start (create-conn))
|
||||
```
|
||||
|
||||
where `(create-conn)` is defined elsewhere, can be right above it.
|
||||
where the `create-conn` function creates a connection (for example to a database) and is defined elsewhere, can be right above it.
|
||||
|
||||
In case this state needs to be cleaned / destryed between reloads, there is also `:stop`
|
||||
In case this state needs to be cleaned / destroyed between reloads, there is also `:stop`
|
||||
|
||||
```clojure
|
||||
(defstate conn :start (create-conn)
|
||||
|
|
@ -80,7 +103,7 @@ That is pretty much it. But wait, there is more.. this state is _a top level bei
|
|||
`required` by other namespaces or in REPL:
|
||||
|
||||
```clojure
|
||||
dev=> (require '[app.nyse :refer [conn]])
|
||||
dev=> (require '[app.db :refer [conn]])
|
||||
nil
|
||||
dev=> conn
|
||||
#object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"]
|
||||
|
|
@ -97,6 +120,22 @@ For example let's say an `app` needs a connection above. No problem:
|
|||
|
||||
where `above` is an arbitrary namespace that defines the above state / connection.
|
||||
|
||||
### Documentation String
|
||||
|
||||
As in any definition (i.e. `def`, `defn`) a documentation string can be added to better describe a state:
|
||||
|
||||
```clojure
|
||||
(defstate answer
|
||||
"answer to the ultimate question of life universe and everything"
|
||||
:start (+ 1 41))
|
||||
```
|
||||
```clojure
|
||||
(doc answer)
|
||||
-------------------------
|
||||
dev/answer
|
||||
answer to the ultimate question of life universe and everything
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
If the whole app is one big application context (or `system`), cross dependencies with a solid dependency graph
|
||||
|
|
@ -109,47 +148,89 @@ If a managing state library requires a whole app buy-in, where everything is a b
|
|||
it is a framework, and dependency graph is usually quite large and complex,
|
||||
since it has _everything_ (every piece of the application) in it.
|
||||
|
||||
But if stateful things are kept lean and low level (i.e. I/O, queues, etc.), dependency graphs are simple
|
||||
and small, and everything else is just namespaces and functions: the way it should be.
|
||||
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.
|
||||
|
||||
### Talking States
|
||||
|
||||
There are of course direct dependecies that `mount` respects:
|
||||
There are of course direct dependencies that `mount` respects:
|
||||
|
||||
```clojure
|
||||
(ns app.config
|
||||
(:require [mount :refer [defstate]]))
|
||||
(:require [mount.core :refer [defstate]]))
|
||||
|
||||
(defstate app-config
|
||||
(defstate config
|
||||
:start (load-config "test/resources/config.edn"))
|
||||
```
|
||||
|
||||
this `app-config`, being top level, can be used in other namespaces, including the ones that create states:
|
||||
this `config`, being top level, can be used in other namespaces, including the ones that create states:
|
||||
|
||||
```clojure
|
||||
(ns app.database
|
||||
(:require [mount :refer [defstate]]
|
||||
[app.config :refer [app-config]]))
|
||||
(:require [mount.core :refer [defstate]]
|
||||
[app.config :refer [config]]))
|
||||
|
||||
(defstate conn :start (create-connection app-config))
|
||||
(defstate conn :start (create-connection config))
|
||||
```
|
||||
|
||||
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)
|
||||
is an example of a Datomic connection that "depends" on a similar `app-config`.
|
||||
[here](dev/clj/app/www.clj#L32) is an example of a web server that "depends" on a similar `config`.
|
||||
|
||||
###### _(the example `load-config` function above comes from [cprop](https://github.com/tolitius/cprop), but could of course be a custom function that loads configuration from a file)_
|
||||
|
||||
## Value of values
|
||||
|
||||
Lifecycle functions start/stop can take both functions and values. This is "valuable" and also works:
|
||||
|
||||
```clojure
|
||||
(defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Besides scalar values, lifecycle functions can take anonymous functions, partial functions, function references, etc.. Here are some examples:
|
||||
|
||||
```clojure
|
||||
(defn f [n]
|
||||
(fn [m]
|
||||
(+ n m)))
|
||||
|
||||
(defn g [a b]
|
||||
(+ a b))
|
||||
|
||||
(defn- pf [n]
|
||||
(+ 41 n))
|
||||
|
||||
(defn fna []
|
||||
42)
|
||||
|
||||
(defstate scalar :start 42)
|
||||
(defstate fun :start #(inc 41))
|
||||
(defstate with-fun :start (inc 41))
|
||||
(defstate with-partial :start (partial g 41))
|
||||
(defstate f-in-f :start (f 41))
|
||||
(defstate f-no-args-value :start (fna))
|
||||
(defstate f-no-args :start fna)
|
||||
(defstate f-args :start g)
|
||||
(defstate f-value :start (g 41 1))
|
||||
(defstate private-f :start pf)
|
||||
```
|
||||
|
||||
Check out [fun-with-values-test](test/core/mount/test/fun_with_values.cljc) for more details.
|
||||
|
||||
## The Importance of Being Reloadable
|
||||
|
||||
`mount` has start and stop functions that will walk all the states created with `defstate` and start / stop them
|
||||
accordingly: i.e. will call their `:start` and `:stop` defined functions. Hence the whole applicatoin state can be reloaded in REPL e.g.:
|
||||
accordingly: i.e. will call their `:start` and `:stop` defined functions. Hence the whole application state can be reloaded in REPL e.g.:
|
||||
|
||||
```
|
||||
dev=> (require '[mount.core :as mount])
|
||||
|
||||
dev=> (mount/stop)
|
||||
dev=> (mount/start)
|
||||
```
|
||||
|
||||
This can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace), to make the whole
|
||||
application reloadable with refreshing the app namespaces. Here is a [dev.clj](https://github.com/tolitius/mount/blob/master/dev/dev.clj) as
|
||||
an example, that sums up to:
|
||||
While it is not always necessary, mount lifecycle can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace),
|
||||
to make the whole application reloadable with refreshing the app namespaces.
|
||||
Here is a [dev.clj](dev/clj/dev.clj) as an example, that sums up to:
|
||||
|
||||
```clojure
|
||||
(defn go []
|
||||
|
|
@ -161,7 +242,7 @@ an example, that sums up to:
|
|||
(tn/refresh :after 'dev/go))
|
||||
```
|
||||
|
||||
the `(reset)` is then used in REPL to restart / relaod application state without the need to restart the REPL itself.
|
||||
the `(reset)` is then used in REPL to restart / reload application state without the need to restart the REPL itself.
|
||||
|
||||
## Start and Stop Order
|
||||
|
||||
|
|
@ -176,33 +257,457 @@ The "stop" order is simply `(reverse "start order")`:
|
|||
dev=> (reset)
|
||||
08:21:39.430 [nREPL-worker-1] DEBUG mount - << stopping.. nrepl
|
||||
08:21:39.431 [nREPL-worker-1] DEBUG mount - << stopping.. conn
|
||||
08:21:39.432 [nREPL-worker-1] DEBUG mount - << stopping.. app-config
|
||||
08:21:39.432 [nREPL-worker-1] DEBUG mount - << stopping.. config
|
||||
|
||||
:reloading (app.config app.nyse app.utils.datomic app)
|
||||
|
||||
08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting.. app-config
|
||||
08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting.. config
|
||||
08:21:39.463 [nREPL-worker-1] DEBUG mount - >> starting.. conn
|
||||
08:21:39.481 [nREPL-worker-1] DEBUG mount - >> starting.. nrepl
|
||||
:ready
|
||||
```
|
||||
|
||||
You can see examples of start and stop flows in the [example app](https://github.com/tolitius/mount#mount-and-develop).
|
||||
You can see examples of start and stop flows in the [example app](README.md#mount-and-develop).
|
||||
|
||||
## Composing States
|
||||
|
||||
Besides calling `(mount/start)` there are other useful ways to start an application:
|
||||
|
||||
* [starting parts of an application](README.md#start-and-stop-parts-of-application)
|
||||
* [starting an application without certain states](README.md#start-an-application-without-certain-states)
|
||||
* [swapping alternate implementations](README.md#swapping-alternate-implementations)
|
||||
* [passing runtime arguments](README.md#runtime-arguments)
|
||||
|
||||
While all of these are great by themselves, sometimes it is really handy to compose these super powers. For example to start an application with _only_ certain states, _swapping_ a couple of them for new values, while passing runtime _arguments_.
|
||||
|
||||
### Composer's Toolbox
|
||||
|
||||
Each "tool" has a single responsibility and can be composed with other tools in _any_ combination and order.
|
||||
|
||||
* `only` will return _only_ states that it is given + exist (seen by mount) in the application
|
||||
* `except` will return all the states that it is given _except_ a given set
|
||||
* `swap` will take a map with keys as states and values as their substitute values
|
||||
* `swap-states` will take a map with keys as states and values with `{:start fn :stop fn}` as their substitute states
|
||||
* `with-args` will take a map that could later be accessed by `(mount/args)`
|
||||
|
||||
All these functions take one or two arguments. If called with two arguments, the first one will be treated as the universe of states to work with. If called with one argument, it will work with _all known_ to mount states.
|
||||
|
||||
None of these functions start or stop the application states, they merely serve as transformations from the initial set of states to the one that will later be passed to `(mount/start)`.
|
||||
|
||||
### Be Composing
|
||||
|
||||
All of the above is much easier to understand by looking at examples:
|
||||
|
||||
```clojure
|
||||
(-> (only #{#'foo/a
|
||||
#'foo/b
|
||||
#'foo/c
|
||||
#'bar/d
|
||||
#'baz/e})
|
||||
(except [#'foo/c
|
||||
#'bar/d])
|
||||
(with-args {:a 42})
|
||||
mount/start)
|
||||
```
|
||||
|
||||
This would start off from 5 states, even though the whole application may have many more states available. It would then exclude two states (i.e. `#'foo/c` and `#'bar/d`), then it will pass runtime arguments `{:a 42}`, and finally it will start the remaining three states: `#'foo/a`, `#'foo/b`, `#'baz/e`.
|
||||
|
||||
You may notice that `only` takes a set, while `except` takes a vector in this example. This is done intentionally to demonstrate that both these functions can take any collection of states. `set` would make more sense for most cases though.
|
||||
|
||||
Here is a more "involved" example:
|
||||
|
||||
```clojure
|
||||
(-> (only #{#'foo/a
|
||||
#'foo/b
|
||||
#'foo/c
|
||||
#'bar/d
|
||||
#'baz/e})
|
||||
(with-args {:a 42})
|
||||
(except [#'foo/c
|
||||
#'bar/d])
|
||||
(swap-states {#'foo/a {:start #(create-connection test-conf)
|
||||
:stop #(disconnect a)}})
|
||||
(swap {#'baz/e {:datomic {:uri "datomic:mem://composable-mount"}}})
|
||||
mount/start)
|
||||
```
|
||||
|
||||
This will do the same thing as the previous example plus it would swap `#'foo/a` with alternative `:start` and `:stop` functions and `#'baz/e` with `{:datomic {:uri "datomic:mem://composable-mount"}}` value before starting the application.
|
||||
|
||||
## Start and Stop Parts of Application
|
||||
|
||||
In REPL or during testing it is often very useful to work with / start / stop _only a part_ of an application, i.e. "only these two states".
|
||||
|
||||
`mount`'s lifecycle functions, i.e. start/stop, can _optionally_ take states as vars (i.e. prefixed with their namespaces):
|
||||
|
||||
```clojure
|
||||
(mount/start #'app.config/config #'app.nyse/conn)
|
||||
...
|
||||
(mount/stop #'app.config/config #'app.nyse/conn)
|
||||
```
|
||||
|
||||
which will _only_ start/stop `config` and `conn` (won't start/stop any other states).
|
||||
|
||||
Here is an [example](test/core/mount/test/parts.cljc) test that uses only two namespaces checking that the third one is not started.
|
||||
|
||||
## Start an Application Without Certain States
|
||||
|
||||
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.
|
||||
|
||||
The `start-without` function can do just that:
|
||||
|
||||
```clojure
|
||||
(mount/start-without #'app.feeds/feed-listener
|
||||
#'app/nrepl)
|
||||
```
|
||||
|
||||
which will start an application without starting `feed-listener` and `nrepl` states.
|
||||
|
||||
Here is an [example](test/core/mount/test/start_without.cljc) test that excludes Datomic connection and nREPL from an application on start.
|
||||
|
||||
## Swapping Alternate Implementations
|
||||
|
||||
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.
|
||||
|
||||
### Swapping States with Values
|
||||
|
||||
The `start-with` function takes values as substitutes.
|
||||
|
||||
Say we have a `send-sms` state:
|
||||
|
||||
```clojure
|
||||
(ns app.sms)
|
||||
;; ...
|
||||
(defstate send-sms :start (create-sms-sender
|
||||
(:sms config)))
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```clojure
|
||||
(let [sms-ch (chan)
|
||||
send-sms (fn [sms] (go (>! sms-ch sms)))]
|
||||
(mount/start-with {#'app.sms/send-sms send-sms}) ;; <<<< swapping the "send-sms" state with a test function
|
||||
;; testing.. checking "sms-ch" channel
|
||||
(mount/stop))
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
### Swapping States with States
|
||||
|
||||
The `start-with-states` function takes values in a form of `{:start fn :stop fn}` as substitutes:
|
||||
|
||||
```clojure
|
||||
(mount/start-with-states {#'app.neo/db {:start #(connect test-config)
|
||||
:stop #(disconnect db)}
|
||||
#'app.neo/publisher {:start #(create-pub test-config)
|
||||
:stop #(close-pub publisher)}})
|
||||
```
|
||||
|
||||
`start-with-states` takes a map of states with their substitutes. For example `#'app.nyse/db` here is the real deal (remote) DB that is being
|
||||
substituted with `#(connect test-config)` function, which could end up being anything, a map, an in memory DB, etc.
|
||||
|
||||
The `:stop` functions of substitutes can be anything, and could refer to the original state references. As in the example above: `db` and `publisher`
|
||||
are real references. They would need to be accessible from the namespace of course, so you might need to `(:require [app.neo :refer [db]])`
|
||||
in order to use `db` in `:stop #(disconnect db)` example above.
|
||||
|
||||
--
|
||||
|
||||
One thing to note is whenever
|
||||
|
||||
```clojure
|
||||
(mount/stop)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Here is an [example](test/core/mount/test/start_with_states.cljc) test that starts an app with mocking Datomic connection and nREPL.
|
||||
|
||||
## Stop an Application Except Certain States
|
||||
|
||||
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)`.
|
||||
|
||||
Here is an example of restarting the application without bringing down `#'app.www/nyse-app`:
|
||||
|
||||
```clojure
|
||||
dev=> (mount/start)
|
||||
14:34:10.813 [nREPL-worker-0] INFO mount.core - >> starting.. config
|
||||
14:34:10.814 [nREPL-worker-0] INFO mount.core - >> starting.. conn
|
||||
14:34:10.814 [nREPL-worker-0] INFO app.db - creating a connection to datomic: datomic:mem://mount
|
||||
14:34:10.838 [nREPL-worker-0] INFO mount.core - >> starting.. nyse-app
|
||||
14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:4242
|
||||
14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED org.eclipse.jetty.server.Server@194f37af
|
||||
14:34:10.844 [nREPL-worker-0] INFO mount.core - >> starting.. nrepl
|
||||
:started
|
||||
|
||||
dev=> (mount/stop-except #'app.www/nyse-app)
|
||||
14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. nrepl
|
||||
14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. conn
|
||||
14:34:47.766 [nREPL-worker-0] INFO app.db - disconnecting from datomic:mem://mount
|
||||
14:34:47.766 [nREPL-worker-0] INFO mount.core - << stopping.. config
|
||||
:stopped
|
||||
dev=>
|
||||
|
||||
dev=> (mount/start)
|
||||
14:34:58.673 [nREPL-worker-0] INFO mount.core - >> starting.. config
|
||||
14:34:58.674 [nREPL-worker-0] INFO app.config - loading config from test/resources/config.edn
|
||||
14:34:58.674 [nREPL-worker-0] INFO mount.core - >> starting.. conn
|
||||
14:34:58.674 [nREPL-worker-0] INFO app.db - creating a connection to datomic: datomic:mem://mount
|
||||
14:34:58.693 [nREPL-worker-0] INFO mount.core - >> starting.. nrepl
|
||||
:started
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Recompiling Namespaces with Running States
|
||||
|
||||
Mount will detect when a namespace with states (i.e. with `(defstate ...)`) was reloaded/recompiled,
|
||||
and will check every state in this namespace whether it was running at the point of recompilation. If it was, _it will restart it_:
|
||||
|
||||
* if a state has a `:stop` function, mount will invoke it on the old version of state (i.e. cleanup)
|
||||
* it will call a "new" `:start` function _after_ this state is recompiled/redefined
|
||||
|
||||
Mount won't keep it a secret, it'll tell you about all the states that had to be restarted during namespace reload/recompilation:
|
||||
|
||||
<img src="doc/img/ns-recompile.png" width="500px">
|
||||
|
||||
same is true for recompiling and reloading (figwheel, boot-reload, etc.) namespaces in ClojureScript:
|
||||
|
||||
<img src="doc/img/cljs-ns-reload.png" width="500px">
|
||||
|
||||
Providing a `:stop` function _is_ optional, but in case a state needs to be cleaned between restarts or on a system shutdown,
|
||||
`:stop` is highly recommended.
|
||||
|
||||
### :on-reload
|
||||
|
||||
By default a state will be restarted on its redefinition or a namespace recompilation. However it is not always a desired behavior. Sometimes it's ok to have stale references during REPL sessions / development, other times all that is needed is not a "restart", but just a "stop".
|
||||
|
||||
This behavior could be controlled with an optional `:on-reload` meta attribute when defining a state.
|
||||
|
||||
In case _nothing_ needs to be done to a running state on reload / recompile / redef, set `:on-reload` to `:noop`:
|
||||
|
||||
```clojure
|
||||
(defstate ^{:on-reload :noop}
|
||||
mem-db :start (connect config)
|
||||
:stop (disconnect mem-db))
|
||||
```
|
||||
|
||||
When a running state needs to be just "stopped" on reload, set `:on-reload` to `:stop`:
|
||||
|
||||
```clojure
|
||||
(defstate ^{:on-reload :stop}
|
||||
mem-db :start (connect config)
|
||||
:stop (disconnect mem-db))
|
||||
```
|
||||
|
||||
Again, by default, if no `:on-reload` meta is added, internally it would be set to `:restart`, in which case a running state will be restarted on a redef / a namespace reload.
|
||||
|
||||
Note that `^{:on-reload :noop}` will disable stopping or starting the state on namespace recompilation but it will still obey `(mount/start)` / `(mount/stop)` calls. This means that if any of the namespaces with `(mount/start)` / `(mount/stop)` calls are reloaded or these calls are explicitely executed (i.e. somewhere in the `dev` namespace or in an `:after` clause), the state's start/stop functions will still be called.
|
||||
|
||||
## Cleaning up Deleted States
|
||||
|
||||
Mount will detect when a state was renamed/deleted from a namespace, and will do two things:
|
||||
|
||||
* if a state had a `:stop` function, mount will invoke it on the old version of state (i.e. cleanup)
|
||||
* will remove any knowledge of this state internally
|
||||
|
||||
Here is an example:
|
||||
|
||||
```clojure
|
||||
dev=> (defstate won't-be-here-long :start (println "I am starting... ")
|
||||
:stop (println "I am stopping... "))
|
||||
#'dev/won't-be-here-long
|
||||
dev=>
|
||||
|
||||
dev=> (mount/start #'dev/won't-be-here-long)
|
||||
INFO app.utils.logging - >> starting.. #'dev/won't-be-here-long
|
||||
I am starting...
|
||||
{:started ["#'dev/won't-be-here-long"]}
|
||||
dev=>
|
||||
```
|
||||
|
||||
"deleting" it from REPL, and starting all the states:
|
||||
|
||||
```clojure
|
||||
dev=> (ns-unmap 'dev 'won't-be-here-long)
|
||||
nil
|
||||
dev=> (mount/start)
|
||||
|
||||
"<< stopping.. #'dev/won't-be-here-long (it was deleted)"
|
||||
I am stopping...
|
||||
|
||||
INFO app.utils.logging - >> starting.. #'app.conf/config
|
||||
INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
INFO app.utils.logging - >> starting.. #'app.example/nrepl
|
||||
{:started ["#'app.conf/config" "#'app.db/conn" "#'app.www/nyse-app" "#'app.example/nrepl"]}
|
||||
```
|
||||
|
||||
Mount detected that `#'dev/won't-be-here-long` was deleted, hence:
|
||||
```clojure
|
||||
<< stopping.. #'dev/won't-be-here-long (it was deleted)
|
||||
```
|
||||
|
||||
## `cljc` mode
|
||||
|
||||
By default mount states are kept under var references. While it works for Clojure, it falls short in the land of ClojureScript since, especially during an `:advanced` compilation, var names get compressed + ClojureScript does not support reified vars.
|
||||
|
||||
To support both Clojure and ClojureScript mount has a `cljc` mode which is well documented in [here](doc/clojurescript.md#managing-state-in-clojurescript), and can be enabled by `(mount/in-cljc-mode)`.
|
||||
|
||||
### Disable Lazy Start
|
||||
|
||||
When in `cljc` mode, mount states that are not started by `(mount/start a b c)`, or that are not transitive states: i.e. not `:require`d at the time `(mount/start)` is called, will start lazily whenever they are dereferenced:
|
||||
|
||||
```clojure
|
||||
=> (mount/in-cljc-mode)
|
||||
:cljc
|
||||
|
||||
=> (defstate db-connection :start (println "connecting")
|
||||
:stop (println "disconnecting..."))
|
||||
|
||||
=> db-connection
|
||||
#object[mount.core.DerefableState 0x546b9d51 {:status :pending, :val nil}]
|
||||
|
||||
dev=> (mount/running-states)
|
||||
#{}
|
||||
|
||||
dev=> @db-connection ;; db-connection will start here when deref'ed even though it was not started explicitly
|
||||
connecting
|
||||
|
||||
dev=> (mount/running-states)
|
||||
#{"#'dev/db-connection"}
|
||||
```
|
||||
|
||||
This can be quite handy as it allows certain app states to start lazily.
|
||||
|
||||
However there are cases when it is best to fail in case a certain state is deref'ed while it was not yet started. This is possible by marking such states with `^{:on-lazy-start :throw}` metadata:
|
||||
|
||||
```clojure
|
||||
=> (defstate ^{:on-lazy-start :throw} db-connection :start (do (println "connecting") 42)
|
||||
:stop (println "disconnecting..."))
|
||||
|
||||
=> @db-connection ;; this will throw since db connection is deref'ed before it was started
|
||||
|
||||
java.lang.RuntimeException: :on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} #'dev/db-connection...) and #'dev/db-connection state was not explicitly started before it was deref'ed (i.e. @#'dev/db-connection)
|
||||
|
||||
=> (mount/start #'dev/db-connection)
|
||||
connecting
|
||||
{:started ["#'dev/db-connection"]}
|
||||
|
||||
=> @db-connection
|
||||
42
|
||||
```
|
||||
|
||||
## Packaging
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```clojure
|
||||
(ns my.app
|
||||
(:require [a]
|
||||
[b]
|
||||
[c]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(defn rock-n-roll [] ;; or (defn -main [args].. )
|
||||
(mount/start))
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## Affected States
|
||||
|
||||
Every time a lifecycle function (start/stop) is called mount will return all the states that were affected:
|
||||
|
||||
```clojure
|
||||
dev=> (mount/start)
|
||||
{:started [#'app.config/config
|
||||
#'app.nyse/conn
|
||||
#'app/nrepl]}
|
||||
```
|
||||
```clojure
|
||||
dev=> (mount/stop)
|
||||
{:stopped [#'app/nrepl
|
||||
#'app.nyse/conn
|
||||
#'app.config/config]}
|
||||
```
|
||||
|
||||
An interesting bit here is a vector vs. a set: all the states are returned _in the order they were affected_.
|
||||
|
||||
## Logging
|
||||
|
||||
> All the mount examples have `>> starting..` / `<< stopping..` logging messages, but when I develop an application with mount I don't see them.
|
||||
|
||||
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.
|
||||
|
||||
Since mount is a _library_ it should _not_ bring any dependencies unless its functionality directly depends on them.
|
||||
|
||||
> _But I still these logging statements in the examples..._
|
||||
|
||||
### mount-up
|
||||
|
||||
One way to do that would be using "[mount-up](https://github.com/tolitius/mount-up)" that "watches mount's ups and downs":
|
||||
|
||||
```clojure
|
||||
=> (require '[mount-up.core :as mu])
|
||||
|
||||
=> (mu/on-upndown :info mu/log :before)
|
||||
|
||||
=> (mount/start)
|
||||
INFO mount-up.core - >> starting.. #'boot.user/server
|
||||
{:started ["#'boot.user/server"]}
|
||||
|
||||
=> (mount/stop)
|
||||
INFO mount-up.core - << stopping.. #'boot.user/server
|
||||
{:stopped ["#'boot.user/server"]}
|
||||
```
|
||||
|
||||
### Manual AOP
|
||||
|
||||
Another, a more manual way, would be to do it via an excellent [robert hooke](https://github.com/technomancy/robert-hooke/). Example applications live in `test`, so does the [utility](test/clj/tapp/utils/logging.clj#L42) that adds logging to all the mount's lifecycle functions on start in [dev.clj](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/dev/dev.clj#L21).
|
||||
|
||||
## Exception Handling
|
||||
|
||||
One way to handle exceptions on start/stop would be to simply wrap start/stop functions in `try/catch`.
|
||||
|
||||
Another way would be to use a custom [mount-up](https://github.com/tolitius/mount-up/blob/master/README.md#wrapping) wrapper.
|
||||
|
||||
## Clojure Version
|
||||
|
||||
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.
|
||||
|
||||
## Mount and Develop!
|
||||
|
||||
`mount` comes with an example [app](https://github.com/tolitius/mount/tree/master/test/app)
|
||||
that has 3 states:
|
||||
Besides a [a collection](https://github.com/tolitius/stater) of sample mount applications, mount _sources_ come with two sample apps:
|
||||
|
||||
* Clojure [app](dev/clj/app)
|
||||
* ClojureScript [app](doc/clojurescript.md#mounting-that-clojurescript)
|
||||
|
||||
You can clone mount, jump into a REPL and start playing with these built in apps.
|
||||
|
||||
Below is an example of the Clojure app that comes with mount.
|
||||
|
||||
The app has 4 states:
|
||||
|
||||
* `config`, loaded from the files and refreshed on each `(reset)`
|
||||
* `datamic connection` that uses the config to create itself
|
||||
* `datomic connection` that uses the config to create itself
|
||||
* `nyse web app` which is a web server with compojure routes (i.e. the actual app)
|
||||
* `nrepl` that uses config to bind to host/port
|
||||
|
||||
### Running New York Stock Exchange
|
||||
|
||||
To try it out, clone `mount`, get to REPL and switch to `(dev)`:
|
||||
To try it out, clone `mount`, get to REPL (`boot repl` or `lein repl`) and switch to `(dev)`:
|
||||
|
||||
```clojure
|
||||
$ lein repl
|
||||
$ boot repl
|
||||
|
||||
user=> (dev)
|
||||
#object[clojure.lang.Namespace 0xcf1a0cc "dev"]
|
||||
|
|
@ -213,88 +718,150 @@ start/restart/reset everything using `(reset)`:
|
|||
```clojure
|
||||
dev=> (reset)
|
||||
|
||||
:reloading (app.config app.nyse app.utils.datomic app dev)
|
||||
15:30:32.412 [nREPL-worker-1] DEBUG mount - >> starting.. app-config
|
||||
15:30:32.414 [nREPL-worker-1] INFO app.config - loading config from test/resources/config.edn
|
||||
15:30:32.422 [nREPL-worker-1] DEBUG mount - >> starting.. conn
|
||||
15:30:32.430 [nREPL-worker-1] INFO app.nyse - conf: {:datomic {:uri datomic:mem://mount}, :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}}
|
||||
15:30:32.430 [nREPL-worker-1] INFO app.nyse - creating a connection to datomic: datomic:mem://mount
|
||||
15:30:32.430 [nREPL-worker-1] DEBUG mount - >> starting.. nrepl
|
||||
:reloading (mount.tools.macro mount.core app.utils.logging app.conf app.db app.utils.datomic app.nyse app.www app.example dev)
|
||||
INFO app.utils.logging - >> starting.. #'app.conf/config
|
||||
INFO app.conf - loading config from dev/resources/config.edn
|
||||
INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
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}}
|
||||
INFO app.db - creating a connection to datomic: datomic:mem://mount
|
||||
INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
INFO app.utils.logging - >> starting.. #'app.example/nrepl
|
||||
dev=>
|
||||
```
|
||||
|
||||
everything is started and can be played with:
|
||||
|
||||
```clojure
|
||||
dev=> (create-nyse-schema)
|
||||
dev=> (add-order "GOOG" 665.51M 665.59M 100)
|
||||
dev=> (add-order "GOOG" 665.50M 665.58M 300)
|
||||
dev=> (add-order conn {:ticker "GOOG" :bid 665.51M :offer 665.59M :qty 100})
|
||||
dev=> (add-order conn {:ticker "GOOG" :bid 665.50M :offer 665.58M :qty 300})
|
||||
|
||||
dev=> (find-orders "GOOG")
|
||||
dev=> (find-orders conn "GOOG")
|
||||
({:db/id 17592186045418, :order/symbol "GOOG", :order/bid 665.51M, :order/qty 100, :order/offer 665.59M}
|
||||
{:db/id 17592186045420, :order/symbol "GOOG", :order/bid 665.50M, :order/qty 300, :order/offer 665.58M})
|
||||
```
|
||||
|
||||
once something is changed in the code, or you just need to reload everything, do `(reset)`:
|
||||
since there is also a web server running, we can add orders with HTTP POST (from a different terminal window):
|
||||
|
||||
```clojure
|
||||
$ curl -X POST -d "ticker=TSLA&qty=100&bid=232.38&offer=232.43" "http://localhost:4242/nyse/orders"
|
||||
|
||||
{"added":{"ticker":"TSLA","qty":"100","bid":"232.38","offer":"232.43"}}
|
||||
```
|
||||
|
||||
```clojure
|
||||
dev=> (find-orders conn "TSLA")
|
||||
({:db/id 17592186045422, :order/symbol "TSLA", :order/bid 232.38M, :order/qty 100, :order/offer 232.43M})
|
||||
```
|
||||
|
||||
once something is changed in the code, or you just need to reload everything, do `(reset)`.
|
||||
|
||||
_note: a simple `(mount/stop)` / `(mount/start)` will also work, `(reset)` is for "convenience + ns refresh":_
|
||||
|
||||
```clojure
|
||||
dev=> (reset)
|
||||
15:32:44.342 [nREPL-worker-2] DEBUG mount - << stopping.. nrepl
|
||||
15:32:44.343 [nREPL-worker-2] DEBUG mount - << stopping.. conn
|
||||
15:32:44.343 [nREPL-worker-2] INFO app.nyse - disconnecting from datomic:mem://mount
|
||||
15:32:44.344 [nREPL-worker-2] DEBUG mount - << stopping.. app-config
|
||||
INFO app.utils.logging - << stopping.. #'app.example/nrepl
|
||||
INFO app.utils.logging - << stopping.. #'app.www/nyse-app
|
||||
INFO app.utils.logging - << stopping.. #'app.db/conn
|
||||
INFO app.db - disconnecting from datomic:mem://mount
|
||||
INFO app.utils.logging - << stopping.. #'app.conf/config
|
||||
|
||||
:reloading (app.config app.nyse app.utils.datomic app dev)
|
||||
:reloading (app.conf app.db app.nyse app.www app.example dev)
|
||||
|
||||
15:32:44.371 [nREPL-worker-2] DEBUG mount - >> starting.. app-config
|
||||
15:32:44.372 [nREPL-worker-2] INFO app.config - loading config from test/resources/config.edn
|
||||
15:32:44.380 [nREPL-worker-2] DEBUG mount - >> starting.. conn
|
||||
15:32:44.382 [nREPL-worker-2] INFO app.nyse - conf: {:datomic {:uri datomic:mem://mount}, :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}}
|
||||
15:32:44.382 [nREPL-worker-2] INFO app.nyse - creating a connection to datomic: datomic:mem://mount
|
||||
15:32:44.387 [nREPL-worker-2] DEBUG mount - >> starting.. nrepl
|
||||
INFO app.utils.logging - >> starting.. #'app.conf/config
|
||||
INFO app.conf - loading config from dev/resources/config.edn
|
||||
INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
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}}
|
||||
INFO app.db - creating a connection to datomic: datomic:mem://mount
|
||||
INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
INFO app.utils.logging - >> starting.. #'app.example/nrepl
|
||||
:ready
|
||||
```
|
||||
|
||||
notice that it stopped and started again.
|
||||
|
||||
In nyse's connection [:stop](https://github.com/tolitius/mount/blob/a63c725dcb6afd7ebb65f8a767d69ee0826921e8/test/app/nyse.clj#L18)
|
||||
function database is deleted. Hence after `(reset)` was called the app was brought its starting point: database was created by the
|
||||
[:start](https://github.com/tolitius/mount/blob/a63c725dcb6afd7ebb65f8a767d69ee0826921e8/test/app/nyse.clj#L11) function,
|
||||
but no schema again:
|
||||
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
|
||||
`: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`.
|
||||
|
||||
But again no orders:
|
||||
|
||||
```clojure
|
||||
dev=> (find-orders "GOOG")
|
||||
|
||||
IllegalArgumentExceptionInfo :db.error/not-an-entity Unable to resolve entity: :order/symbol datomic.error/arg (error.clj:57)
|
||||
dev=> (find-orders conn "GOOG")
|
||||
()
|
||||
dev=> (find-orders conn "TSLA")
|
||||
()
|
||||
```
|
||||
|
||||
hence the app is in its "clean" state, and ready to rock and roll as right after the REPL started:
|
||||
|
||||
```clojure
|
||||
dev=> (create-nyse-schema)
|
||||
dev=> (find-orders "GOOG")
|
||||
()
|
||||
dev=> (add-order conn {:ticker "TSLA" :bid 232.381M :offer 232.436M :qty 250})
|
||||
|
||||
dev=> (add-order "AAPL" 111.712M 111.811M 250)
|
||||
dev=> (find-orders conn "TSLA")
|
||||
({:db/id 17592186045418, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M})
|
||||
```
|
||||
|
||||
dev=> (find-orders "AAPL")
|
||||
({:db/id 17592186045418, :order/symbol "AAPL", :order/bid 111.712M, :order/qty 250, :order/offer 111.811M})
|
||||
### New York Stock Exchange Maintenance
|
||||
|
||||
Say we want to leave the exchange functioning, but would like to make sure that no one can hit it from the web. Easy, just stop the web server:
|
||||
|
||||
```clojure
|
||||
dev=> (mount/stop #'app.www/nyse-app)
|
||||
INFO app.utils.logging - << stopping.. #'app.www/nyse-app
|
||||
{:stopped ["#'app.www/nyse-app"]}
|
||||
dev=>
|
||||
```
|
||||
```bash
|
||||
$ curl localhost:4242
|
||||
curl: (7) Failed to connect to localhost port 4242: Connection refused
|
||||
```
|
||||
|
||||
everything but the web server works as before:
|
||||
|
||||
```clojure
|
||||
dev=> (find-orders conn "TSLA")
|
||||
({:db/id 17592186045420, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M})
|
||||
dev=>
|
||||
```
|
||||
|
||||
once we found who `DDoS`ed us on `:4242`, and punished them, we can restart the web server:
|
||||
|
||||
```clojure
|
||||
dev=> (mount/start #'app.www/nyse-app)
|
||||
INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
{:started ["#'app.www/nyse-app"]}
|
||||
dev=>
|
||||
```
|
||||
|
||||
```clojure
|
||||
$ curl localhost:4242
|
||||
welcome to the mount sample app!
|
||||
```
|
||||
|
||||
## Web and Uberjar
|
||||
|
||||
There is an `uberjar` branch with an example of sample webapp and it's uberjar sibling. Before trying it:
|
||||
There is an `uberjar` branch with an example webapp and it's uberjar sibling. Before trying it:
|
||||
|
||||
```clojure
|
||||
$ git checkout uberjar
|
||||
Switched to branch 'uberjar'
|
||||
```
|
||||
|
||||
The documentation is [here](doc/uberjar.md).
|
||||
The documentation is [here](doc/uberjar.md#creating-reloadable-uberjarable-app).
|
||||
|
||||
## Runtime Arguments
|
||||
|
||||
There is an `with-args` branch with an example app that takes command line params
|
||||
|
||||
```clojure
|
||||
$ git checkout with-args
|
||||
Switched to branch 'with-args'
|
||||
```
|
||||
|
||||
The documentation is [here](doc/runtime-arguments.md#passing-runtime-arguments).
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2015 tolitius
|
||||
Copyright © 2020 tolitius
|
||||
|
||||
Distributed under the Eclipse Public License either version 1.0 or (at
|
||||
your option) any later version.
|
||||
|
|
|
|||
2
boot.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
BOOT_VERSION=2.7.1
|
||||
BOOT_CLOJURE_VERSION=1.10.1
|
||||
155
build.boot
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
(def +version+ "0.1.18")
|
||||
|
||||
;; -- since boot is no more, this file will go away
|
||||
;; -- once deps.edn "learns" how to run all cljs tests
|
||||
|
||||
;; j8; boot test
|
||||
;; j8; boot test-cljs
|
||||
;; j8; boot test-cljs-advanced
|
||||
;; j8; boot -v build-jar push-snapshot
|
||||
;; j8; boot -v build-jar push-release
|
||||
|
||||
(set-env!
|
||||
:source-paths #{"src"}
|
||||
:resource-paths #{"resources"}
|
||||
:dependencies '[;; mount brings _NO DEPENDENCIES_, everything here is for
|
||||
;; mount dev, examples apps and tests
|
||||
|
||||
[org.clojure/clojure "1.8.0" :scope "provided"]
|
||||
[org.clojure/clojurescript "1.7.228" :scope "provided" :classifier "aot"]
|
||||
[datascript "0.15.0" :scope "provided"]
|
||||
[compojure "1.5.0" :scope "provided"]
|
||||
[ring/ring-jetty-adapter "1.1.0" :scope "provided"]
|
||||
[cheshire "5.5.0" :scope "provided"]
|
||||
[hiccups "0.3.0" :scope "provided" :exclusions [org.clojure/clojurescript]]
|
||||
[com.andrewmcveigh/cljs-time "0.3.14" :scope "provided"]
|
||||
[tolitius/yang "0.1.9" :scope "provided"]
|
||||
[ch.qos.logback/logback-classic "1.1.3" :scope "provided"]
|
||||
[org.clojure/tools.logging "0.3.1" :scope "provided"]
|
||||
[robert/hooke "1.3.0" :scope "provided"]
|
||||
[org.clojure/tools.namespace "0.2.11" :scope "provided"]
|
||||
[org.clojure/tools.nrepl "0.2.12" :scope "provided"]
|
||||
[com.datomic/datomic-free "0.9.5359" :scope "provided" :exclusions [joda-time]]
|
||||
|
||||
;; proto repl for fun and joy
|
||||
[proto-repl "0.3.1" :scope "provided"]
|
||||
[proto-repl-charts "0.3.2" :scope "provided"]
|
||||
|
||||
;; boot clj
|
||||
[boot/core "2.7.1" :scope "provided"]
|
||||
[adzerk/bootlaces "0.1.13" :scope "test"]
|
||||
[adzerk/boot-logservice "1.2.0" :scope "test"]
|
||||
[adzerk/boot-test "1.1.1" :scope "test"]
|
||||
[tolitius/boot-check "0.1.2" :scope "test"]
|
||||
|
||||
;; boot cljs
|
||||
[adzerk/boot-cljs "1.7.228-1" :scope "test"]
|
||||
[adzerk/boot-cljs-repl "0.4.0" :scope "test"]
|
||||
[cider/piggieback "0.3.9" :scope "test" :exclusions [org.clojure/clojurescript]]
|
||||
[weasel "0.7.0" :scope "test" :exclusions [org.clojure/clojurescript]]
|
||||
[nrepl "0.4.5" :scope "test"]
|
||||
[pandeiro/boot-http "0.7.1-SNAPSHOT" :scope "test"]
|
||||
[tolitius/boot-stripper "0.1.0-SNAPSHOT" :scope "test"]
|
||||
[adzerk/boot-reload "0.4.8" :scope "test"]
|
||||
[crisptrutski/boot-cljs-test "0.2.1-SNAPSHOT" :scope "test"]])
|
||||
|
||||
(require '[adzerk.bootlaces :refer :all]
|
||||
'[adzerk.boot-test :as bt]
|
||||
'[adzerk.boot-logservice :as log-service]
|
||||
'[adzerk.boot-cljs :refer [cljs]]
|
||||
'[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]]
|
||||
'[adzerk.boot-reload :refer [reload]]
|
||||
'[tolitius.boot-check :as check]
|
||||
'[tolitius.boot-stripper :refer [strip-deps-attr]]
|
||||
'[pandeiro.boot-http :refer :all]
|
||||
'[crisptrutski.boot-cljs-test :as tcs]
|
||||
'[clojure.tools.logging :as log]
|
||||
'[clojure.tools.namespace.repl :refer [set-refresh-dirs]])
|
||||
|
||||
(bootlaces! +version+)
|
||||
|
||||
(def log4b
|
||||
[:configuration
|
||||
[:appender {:name "STDOUT" :class "ch.qos.logback.core.ConsoleAppender"}
|
||||
[:encoder [:pattern "%-5level %logger{36} - %msg%n"]]]
|
||||
[:root {:level "TRACE"}
|
||||
[:appender-ref {:ref "STDOUT"}]]])
|
||||
|
||||
(deftask dev []
|
||||
(set-env! :source-paths #(conj % "dev/clj" "dev/cljs"))
|
||||
|
||||
(alter-var-root #'log/*logger-factory*
|
||||
(constantly (log-service/make-factory log4b)))
|
||||
(apply set-refresh-dirs (get-env :directories))
|
||||
(load-data-readers!)
|
||||
|
||||
(require 'dev)
|
||||
(in-ns 'dev))
|
||||
|
||||
(deftask test []
|
||||
(set-env! :source-paths #(conj % "test/core" "test/clj")) ;; (!) :source-paths must not overlap.
|
||||
(bt/test))
|
||||
|
||||
(deftask test-cljs []
|
||||
(set-env! :source-paths #(conj % "test/core" "test/cljs"))
|
||||
(set-env! :resource-paths #{"test/resources"})
|
||||
|
||||
(require '[mount.core])
|
||||
|
||||
(comp
|
||||
(strip-deps-attr :attr :classifier :value "aot")
|
||||
(tcs/test-cljs ;; :optimizations :advanced
|
||||
:out-file "mount.js")))
|
||||
|
||||
(deftask test-cljs-advanced []
|
||||
(set-env! :source-paths #(conj % "dev/clj" "dev/cljs"))
|
||||
(set-env! :resource-paths #{"dev/resources"})
|
||||
|
||||
(comp
|
||||
(cljs :optimizations :advanced :ids #{"mount"})))
|
||||
|
||||
(deftask check-sources []
|
||||
(set-env! :source-paths #(conj % "dev/clj" "dev/cljs" "test/core" "test/clj" "test/cljs"))
|
||||
;; (load-data-readers!)
|
||||
(comp
|
||||
(check/with-bikeshed)
|
||||
(check/with-eastwood)
|
||||
;; (check/with-yagni) ;; yagni does not yet support (throws on) "cljc"
|
||||
(check/with-kibit)))
|
||||
|
||||
(deftask cljs-dev
|
||||
"mount cljs dev example"
|
||||
[]
|
||||
(set-env! :source-paths #(conj % "dev/clj" "dev/cljs"))
|
||||
(set-env! :resource-paths #{"dev/resources"})
|
||||
|
||||
(comp
|
||||
(serve :dir "dev/resources/public/")
|
||||
(watch)
|
||||
(reload)
|
||||
(cljs-repl)
|
||||
(cljs :optimizations :none :ids #{"mount"})))
|
||||
|
||||
(deftask cljs-example
|
||||
"mount cljs example"
|
||||
[]
|
||||
(set-env! :source-paths #(conj % "dev/clj" "dev/cljs"))
|
||||
(set-env! :resource-paths #{"dev/resources"})
|
||||
|
||||
(comp
|
||||
(wait)
|
||||
(serve :dir "dev/resources/public/")
|
||||
(cljs-repl)
|
||||
(cljs :optimizations :advanced :ids #{"mount"})))
|
||||
|
||||
(task-options!
|
||||
tcs/test-cljs {:js-env :phantom}
|
||||
push {; :ensure-clean nil
|
||||
:ensure-branch nil}
|
||||
pom {:project 'mount
|
||||
:version +version+
|
||||
:description "managing Clojure and ClojureScript app state since (reset)"
|
||||
:url "https://github.com/tolitius/mount"
|
||||
:scm {:url "https://github.com/tolitius/mount"}
|
||||
:license {"Eclipse Public License"
|
||||
"http://www.eclipse.org/legal/epl-v10.html"}})
|
||||
52
deps.edn
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{:paths ["src" "resources"]
|
||||
|
||||
:deps {} ;; deps no deps
|
||||
|
||||
:aliases {:dev {:extra-deps {metosin/jsonista {:mvn/version "0.3.8"}
|
||||
com.datomic/datomic-free {:mvn/version "0.9.5359"
|
||||
:exclusions [joda-time/joda-time]}
|
||||
org.clojure/tools.nrepl {:mvn/version "0.2.12"}
|
||||
org.clojure/tools.namespace {:mvn/version "0.2.11"}
|
||||
cheshire/cheshire {:mvn/version "5.5.0"}
|
||||
compojure/compojure {:mvn/version "1.5.0"}
|
||||
ring/ring-jetty-adapter {:mvn/version "1.1.0"}
|
||||
robert/hooke {:mvn/version "1.3.0"}
|
||||
proto-repl/proto-repl {:mvn/version "0.3.1"}
|
||||
proto-repl-charts/proto-repl-charts {:mvn/version "0.3.2"}
|
||||
nrepl/nrepl {:mvn/version "0.7.0"}}}
|
||||
:test {:extra-paths ["test/core" "test/clj" "test/cljs" "test/resources"]
|
||||
:extra-deps {com.datomic/datomic-free {:mvn/version "0.9.5359"
|
||||
:exclusions [joda-time/joda-time]}
|
||||
org.clojure/tools.nrepl {:mvn/version "0.2.12"}
|
||||
robert/hooke {:mvn/version "1.3.0"}
|
||||
org.clojure/tools.logging {:mvn/version "1.3.0"}
|
||||
io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
|
||||
:sha "e7660458ce25bc4acb4ccc3e2415aae0a4907198"}}
|
||||
:main-opts ["-m" "cognitect.test-runner"]
|
||||
:exec-fn cognitect.test-runner.api/test}
|
||||
:test-cljs {:extra-paths ["test/core" "test/cljs" "test/resources"]
|
||||
:extra-deps {org.clojure/clojure {:mvn/version "1.8.0"}
|
||||
org.clojure/clojurescript {:mvn/version "1.7.228"}
|
||||
com.andrewmcveigh/cljs-time {:mvn/version "0.3.14"}
|
||||
hiccups/hiccups {:mvn/version "0.3.0"}
|
||||
datascript/datascript {:mvn/version "0.15.0"}
|
||||
olical/cljs-test-runner {:mvn/version "3.8.1"}}
|
||||
:main-opts ["-m" "cljs-test-runner.main"]}
|
||||
:repl {:extra-paths ["dev/clj"]
|
||||
:extra-deps {cider/cider-nrepl {:mvn/version "0.22.4"}
|
||||
org.clojure/tools.logging {:mvn/version "1.2.4"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
|
||||
:main-opts ["-e" "(require 'dev)(in-ns 'dev)"
|
||||
"-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"
|
||||
"-i" "-f" "rebel-readline.main/-main"]}
|
||||
:outdated {:extra-deps {olical/depot {:mvn/version "2.0.1"}}
|
||||
:main-opts ["-m" "depot.outdated.main" "-a" "outdated"]}
|
||||
:tag {:extra-deps {tolitius/tag {:mvn/version "0.1.7"}}
|
||||
:main-opts ["-m" "tag.core" "tolitius/mount" "managing Clojure and ClojureScript app state since (reset)"]}
|
||||
:jar {:extra-deps {seancorfield/depstar {:mvn/version "1.1.128"}}
|
||||
:extra-paths ["target/about"]
|
||||
:main-opts ["-m" "hf.depstar.jar" "target/mount.jar" "--exclude" "clojure/core/specs/alpha.*"]}
|
||||
:deploy {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "target/mount.jar"]}
|
||||
:install {:extra-deps {deps-deploy/deps-deploy {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "deps-deploy.deps-deploy" "install" "target/mount.jar"]}}}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
(ns app.config
|
||||
(:require [mount :refer [defstate]]
|
||||
(ns app.conf
|
||||
(:require [mount.core :as mount :refer [defstate]]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.tools.logging :refer [info]]))
|
||||
|
||||
|
|
@ -9,5 +9,5 @@
|
|||
slurp
|
||||
edn/read-string))
|
||||
|
||||
(defstate app-config
|
||||
:start (load-config "test/resources/config.edn"))
|
||||
(defstate config
|
||||
:start (load-config "dev/resources/config.edn"))
|
||||
50
dev/clj/app/db.clj
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
(ns app.db
|
||||
(:require [mount.core :refer [defstate]]
|
||||
[datomic.api :as d]
|
||||
[clojure.tools.logging :refer [info]]
|
||||
[app.conf :refer [config]]))
|
||||
|
||||
(defn- new-connection [conf]
|
||||
(info "conf: " conf)
|
||||
(let [uri (get-in conf [:datomic :uri])]
|
||||
(info "creating a connection to datomic:" uri)
|
||||
(d/create-database uri)
|
||||
(d/connect uri)))
|
||||
|
||||
(defn disconnect [conf conn]
|
||||
(let [uri (get-in conf [:datomic :uri])]
|
||||
(info "disconnecting from " uri)
|
||||
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
|
||||
(d/delete-database uri)))
|
||||
|
||||
(defstate conn :start (new-connection config)
|
||||
:stop (disconnect config conn))
|
||||
|
||||
;; datomic schema (staging for an example)
|
||||
(defn create-schema [conn]
|
||||
(let [schema [{:db/id #db/id [:db.part/db]
|
||||
:db/ident :order/symbol
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/index true
|
||||
:db.install/_attribute :db.part/db}
|
||||
|
||||
{:db/id #db/id [:db.part/db]
|
||||
:db/ident :order/bid
|
||||
:db/valueType :db.type/bigdec
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db.install/_attribute :db.part/db}
|
||||
|
||||
{:db/id #db/id [:db.part/db]
|
||||
:db/ident :order/qty
|
||||
:db/valueType :db.type/long
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db.install/_attribute :db.part/db}
|
||||
|
||||
{:db/id #db/id [:db.part/db]
|
||||
:db/ident :order/offer
|
||||
:db/valueType :db.type/bigdec
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db.install/_attribute :db.part/db}]]
|
||||
|
||||
@(d/transact conn schema)))
|
||||
18
dev/clj/app/example.clj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(ns app.example
|
||||
(:require [clojure.tools.nrepl.server :refer [start-server stop-server]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[app.conf :refer [config]]
|
||||
[app.www])
|
||||
(:gen-class)) ;; for -main / uberjar (no need in dev)
|
||||
|
||||
;; example on creating a network REPL
|
||||
(defn- start-nrepl [{:keys [host port]}]
|
||||
(start-server :bind host :port port))
|
||||
|
||||
;; nREPL is just another simple state
|
||||
(defstate nrepl :start (start-nrepl (:nrepl config))
|
||||
:stop (stop-server nrepl))
|
||||
|
||||
;; example of an app entry point
|
||||
(defn -main [& args]
|
||||
(mount/start))
|
||||
16
dev/clj/app/nyse.clj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
(ns app.nyse
|
||||
(:require [datomic.api :as d]
|
||||
[app.utils.datomic :refer [touch]]))
|
||||
|
||||
(defn add-order [conn {:keys [ticker bid offer qty]}]
|
||||
@(d/transact conn [{:db/id (d/tempid :db.part/user)
|
||||
:order/symbol ticker
|
||||
:order/bid bid
|
||||
:order/offer offer
|
||||
:order/qty qty}]))
|
||||
|
||||
(defn find-orders [conn ticker]
|
||||
(let [orders (d/q '[:find ?e :in $ ?ticker
|
||||
:where [?e :order/symbol ?ticker]]
|
||||
(d/db conn) ticker)]
|
||||
(touch conn orders)))
|
||||
43
dev/clj/app/utils/logging.clj
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
(ns app.utils.logging ;; << change to your namespace/path
|
||||
(:require [mount.core]
|
||||
[robert.hooke :refer [add-hook clear-hooks]]
|
||||
[clojure.string :refer [split]]
|
||||
[clojure.tools.logging :refer [info]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn- f-to-action [f {:keys [status]}]
|
||||
(let [fname (-> (str f)
|
||||
(split #"@")
|
||||
first)]
|
||||
(case fname
|
||||
"mount.core$up" (when-not (:started status) :up)
|
||||
"mount.core$down" (when-not (:stopped status) :down)
|
||||
:noop)))
|
||||
|
||||
(defn whatcha-doing? [action]
|
||||
(case action
|
||||
:up ">> starting"
|
||||
:down "<< stopping"
|
||||
false))
|
||||
|
||||
(defn log-status [f & args]
|
||||
(let [[state-name state] args
|
||||
action (f-to-action f state)]
|
||||
(when-let [taking-over-the-world (whatcha-doing? action)]
|
||||
(info (str taking-over-the-world ".. " state-name)))
|
||||
(apply f args)))
|
||||
|
||||
(defonce lifecycle-fns
|
||||
#{#'mount.core/up
|
||||
#'mount.core/down})
|
||||
|
||||
(defn without-logging-status []
|
||||
(doall (map #(clear-hooks %) lifecycle-fns)))
|
||||
|
||||
|
||||
;; this is the one to use:
|
||||
|
||||
(defn with-logging-status []
|
||||
(without-logging-status)
|
||||
(doall (map #(add-hook % log-status) lifecycle-fns)))
|
||||
33
dev/clj/app/www.clj
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
(ns app.www
|
||||
(:require [app.nyse :refer [add-order find-orders]]
|
||||
[app.db :refer [conn create-schema]]
|
||||
[app.conf :refer [config]]
|
||||
[mount.core :refer [defstate]]
|
||||
[cheshire.core :refer [generate-string]]
|
||||
[compojure.core :refer [routes defroutes GET POST]]
|
||||
[compojure.handler :as handler]
|
||||
[ring.adapter.jetty :refer [run-jetty]]))
|
||||
|
||||
(defroutes mount-example-routes
|
||||
|
||||
(GET "/" [] "welcome to mount sample app!")
|
||||
(GET "/nyse/orders/:ticker" [ticker]
|
||||
(generate-string (find-orders conn ticker)))
|
||||
|
||||
(POST "/nyse/orders" [ticker qty bid offer]
|
||||
(let [order {:ticker ticker
|
||||
:bid (bigdec bid)
|
||||
:offer (bigdec offer)
|
||||
:qty (Integer/parseInt qty)}]
|
||||
(add-order conn order)
|
||||
(generate-string {:added order}))))
|
||||
|
||||
(defn start-nyse [conn {:keys [www]}] ;; app entry point
|
||||
(create-schema conn) ;; just an example, usually schema would already be there
|
||||
(-> (routes mount-example-routes)
|
||||
(handler/site)
|
||||
(run-jetty {:join? false
|
||||
:port (:port www)})))
|
||||
|
||||
(defstate nyse-app :start (start-nyse conn config)
|
||||
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
|
||||
50
dev/clj/dev.clj
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
(ns dev
|
||||
(:require [clojure.pprint :refer [pprint]]
|
||||
[clojure.tools.namespace.repl :as tn]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[mount.tools.graph :refer [states-with-deps]]
|
||||
[app.utils.logging :refer [with-logging-status]]
|
||||
[app.www]
|
||||
[app.db :refer [conn]]
|
||||
[app.example]
|
||||
[app.nyse :refer [find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time
|
||||
|
||||
(defn start []
|
||||
(with-logging-status)
|
||||
(mount/start #'app.conf/config
|
||||
#'app.db/conn
|
||||
#'app.www/nyse-app
|
||||
#'app.example/nrepl)) ;; example on how to start app with certain states
|
||||
|
||||
(defn stop []
|
||||
(mount/stop))
|
||||
|
||||
(defn refresh []
|
||||
(stop)
|
||||
(tn/refresh))
|
||||
|
||||
(defn refresh-all []
|
||||
(stop)
|
||||
(tn/refresh-all))
|
||||
|
||||
(defn go
|
||||
"starts all states defined by defstate"
|
||||
[]
|
||||
(start)
|
||||
:ready)
|
||||
|
||||
(defn reset
|
||||
"stops all states defined by defstate, reloads modified source files, and restarts the states"
|
||||
[]
|
||||
(stop)
|
||||
(tn/refresh :after 'dev/go))
|
||||
|
||||
(mount/in-clj-mode)
|
||||
|
||||
(defn load-data-readers!
|
||||
"Refresh *data-readers* with readers from newly acquired dependencies."
|
||||
[]
|
||||
(#'clojure.core/load-data-readers)
|
||||
(set! *data-readers* (.getRawRoot #'*data-readers*)))
|
||||
|
||||
(load-data-readers!)
|
||||
15
dev/clj/proto_play.clj
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
(ns proto-play
|
||||
(:require [mount.tools.graph :as mg]
|
||||
[proto-repl-charts.graph :as proto]))
|
||||
|
||||
(defn mount->proto [graph]
|
||||
(reduce (fn [g {:keys [name deps]}]
|
||||
(-> g
|
||||
(update :nodes conj name)
|
||||
(update :edges conj (-> deps (conj name) vec))))
|
||||
{}
|
||||
graph))
|
||||
|
||||
(->> (mg/states-with-deps)
|
||||
mount->proto
|
||||
(proto/graph "a proto graph of mount states"))
|
||||
25
dev/cljs/app/audit_log.cljs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
(ns app.audit-log
|
||||
(:require [datascript.core :as d]
|
||||
[cljs-time.core :refer [now]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defstate log :start (d/create-conn {}))
|
||||
|
||||
(defn audit [db source & msg]
|
||||
(d/transact! @db [{:db/id -1
|
||||
:source source
|
||||
:timestamp (now)
|
||||
:msg (apply str msg)}]))
|
||||
|
||||
(defn find-source-logs [db source]
|
||||
(d/q '{:find [?t ?msg]
|
||||
:in [$ ?s]
|
||||
:where [[?e :source ?s]
|
||||
[?e :timestamp ?t]
|
||||
[?e :msg ?msg]]}
|
||||
@@db source))
|
||||
|
||||
(defn find-all-logs [db]
|
||||
(->> (map :e (d/datoms @@db :aevt :timestamp))
|
||||
dedupe
|
||||
(d/pull-many @@db '[:timestamp :source :msg])))
|
||||
9
dev/cljs/app/conf.cljs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
(ns app.conf
|
||||
(:require [app.audit-log :refer [audit log]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defn load-config [path]
|
||||
(audit log :app-conf "loading config from '" path "' (at least pretending)")
|
||||
{:system-a {:uri "ws://echo.websocket.org/"}})
|
||||
|
||||
(defstate config :start (load-config "resources/config.end"))
|
||||
26
dev/cljs/app/example.cljs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
(ns app.example
|
||||
(:require [mount.core :as mount]
|
||||
[app.conf]
|
||||
[app.websockets]
|
||||
[app.audit-log :refer [log find-all-logs]]
|
||||
[cljs-time.format :refer [unparse formatters]]
|
||||
[hiccups.runtime :as hiccupsrt])
|
||||
(:require-macros [hiccups.core :as hiccups :refer [html]]))
|
||||
|
||||
(defn format-log-event [{:keys [timestamp source msg]}]
|
||||
(str (unparse (formatters :date-hour-minute-second-fraction) timestamp)
|
||||
" → [" (name source) "]: " msg))
|
||||
|
||||
(defn show-log []
|
||||
(.write js/document
|
||||
(html [:ul (doall (for [e (find-all-logs log)]
|
||||
[:li (format-log-event e)]))])))
|
||||
|
||||
(mount/start)
|
||||
|
||||
;; time to establish a websocket connection before disconnecting
|
||||
(js/setTimeout #(mount/stop-except "#'app.audit-log/log") 500)
|
||||
|
||||
;; time to close a connection to show it in audit
|
||||
(js/setTimeout #(show-log) 1000)
|
||||
|
||||
22
dev/cljs/app/websockets.cljs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
(ns app.websockets
|
||||
(:require [app.conf :refer [config]]
|
||||
[app.audit-log :refer [audit log]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defn ws-status [ws]
|
||||
{:url (.-url ws) :ready-state (.-readyState ws)})
|
||||
|
||||
(defn connect [uri]
|
||||
(let [ws (js/WebSocket. uri)]
|
||||
(audit log :system-a "connecting to " (ws-status ws))
|
||||
(set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws)))
|
||||
(set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws)))
|
||||
ws))
|
||||
|
||||
(defn disconnect [ws]
|
||||
(audit log :system-a "closing " (ws-status @ws))
|
||||
(.close @ws)
|
||||
(audit log :system-a "disconnecting " (ws-status @ws)))
|
||||
|
||||
(defstate system-a :start (connect (get-in @config [:system-a :uri]))
|
||||
:stop (disconnect system-a))
|
||||
43
dev/dev.clj
|
|
@ -1,43 +0,0 @@
|
|||
(ns dev
|
||||
"Tools for interactive development with the REPL. This file should
|
||||
not be included in a production build of the application."
|
||||
;; (:use [cljs.repl :only [repl]]
|
||||
;; [cljs.repl.browser :only [repl-env]])
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.java.javadoc :refer [javadoc]]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.reflect :refer [reflect]]
|
||||
[clojure.repl :refer [apropos dir doc find-doc pst source]]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :as test]
|
||||
;; [clojure.core.async :refer [>!! <!! >! <! go-loop alt! timeout]]
|
||||
[clojure.tools.namespace.repl :as tn]
|
||||
[mount]
|
||||
[app :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time
|
||||
|
||||
(defn start []
|
||||
(mount/start))
|
||||
|
||||
(defn stop []
|
||||
(mount/stop))
|
||||
|
||||
(defn refresh []
|
||||
(stop)
|
||||
(tn/refresh))
|
||||
|
||||
(defn refresh-all []
|
||||
(stop)
|
||||
(tn/refresh-all))
|
||||
|
||||
(defn go
|
||||
"starts all states defined by defstate"
|
||||
[]
|
||||
(start)
|
||||
:ready)
|
||||
|
||||
(defn reset
|
||||
"stops all states defined by defstate, reloads modified source files, and restarts the states"
|
||||
[]
|
||||
(stop)
|
||||
(tn/refresh :after 'dev/go))
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
{:datomic
|
||||
{:uri "datomic:mem://mount"}
|
||||
|
||||
:www {:port 4242}
|
||||
|
||||
:h2
|
||||
{:classname "org.h2.Driver"
|
||||
:subprotocol "h2"
|
||||
1
dev/resources/mount.cljs.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
{:require [app.example]}
|
||||
6
dev/resources/public/index.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script src="../mount.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
(defn dev
|
||||
[]
|
||||
(defn dev []
|
||||
(require 'dev)
|
||||
(in-ns 'dev))
|
||||
|
|
|
|||
130
doc/clojurescript.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
## Managing state in ClojureScript
|
||||
|
||||
- [The "Why"](#the-why)
|
||||
- [Mount Modes](#mount-modes)
|
||||
- [Just Clojure Mode](#just-clojure-mode)
|
||||
- [Clojure and ClojureScript Mode](#clojure-and-clojurescript-mode)
|
||||
- [Mounting that ClojureScript](#mounting-that-clojurescript)
|
||||
- [Using States](#using-states)
|
||||
- [Thanks](#thanks)
|
||||
|
||||
In case you need to manage state in ClojureScript using mount, _all_ the mount Clojure features are supported in ClojureScript.
|
||||
Which means all the mount Clojure [documentation](../README.md) is the mount ClojureScript documentation.
|
||||
|
||||
With a slight change in [_mode_](clojurescript.md#mount-modes) ( no change in _mood_ though, just the _mode_ :)).
|
||||
|
||||
### The "Why"
|
||||
|
||||
Since [reader conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) were added in Clojure 1.7,
|
||||
it became a lot easier to target both platforms with lots of code reuse. You might have noticed
|
||||
that most of mount code lives in `.cljc` files.
|
||||
|
||||
The way mount is designed it "mounts" itself to a solid Clojure [namespace API](http://clojure.org/namespaces),
|
||||
and while `.cljc` helps a lot with targeting Clojure and ClojureScript, JavaScript VM is vastly different from JVM.
|
||||
Since JavaScript mostly tagrets browsers, mobile devices and IoT,
|
||||
it is quite important to [compress](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation) the final result.
|
||||
|
||||
Which means that Clojure namespaces API are not that well supported in ClojureScript, since they get renamed and optimized
|
||||
during compilation + of course no native namespace support on the JavaScript side
|
||||
(but that is somewhat solved with [Google Closure](https://closure-library.googlecode.com/git-history/docs/local_closure_goog_base.js.source.html#line428)).
|
||||
|
||||
But. When developing an application in Clojure and ClojureScript, it would only make sense if the API for any library
|
||||
would be _identical_ for both platforms. It should be transparent for developers whether they use a library in Clojure or ClojureScript.
|
||||
It is not possible for all libraries (i.e. concurrency, reified Vars, etc.), but we should try to make it possible for most.
|
||||
|
||||
### Mount Modes
|
||||
|
||||
Mount has two modes `clj` and `cljc`.
|
||||
|
||||
#### Just Clojure Mode
|
||||
|
||||
`clj` mode is _default_, and all the APIs are exactly the same as they are in the mount Clojure [documentation](../README.md).
|
||||
|
||||
#### Clojure _and_ ClojureScript Mode
|
||||
|
||||
`cljc` is not a default mode, but it is easy to switch to:
|
||||
|
||||
To switch Mount into this mode do:
|
||||
|
||||
```clojure
|
||||
(mount/in-cljc-mode)
|
||||
```
|
||||
|
||||
anywhere before a call to `(mount/start)`, usually at the entry point of an app: in the `-main`, web handler, etc.
|
||||
|
||||
This sets mount into the `cljc` mode. In this mode mount supports _both_: Clojure and ClojureScript with one difference
|
||||
from the default `clj` mode:
|
||||
|
||||
> all states are "_derefable_"
|
||||
|
||||
which means in order to use them, you'd need to `@` it. That's where the difference between the two modes ends.
|
||||
|
||||
Again, `cljc` mode API is _consistent across both_ Clojure and ClojureScript.
|
||||
|
||||
While initially it may sound strange, this approach has very nice properties:
|
||||
|
||||
* Mentally something that you deref (`@`) is associated with a state behind it
|
||||
* The whole system may start lazily without an explicit call `(mount/start)`
|
||||
* States may have watchers which is just an idea at this point, but it could be quite useful
|
||||
|
||||
No need to call `(mount/in-cljc-mode)` on ClojureScript side, it is only called once on the server (Clojure) side.
|
||||
|
||||
_note: `(mount/in-cljc-mode)` does not require the code to be `.cljc`, just a geeky name to convey the support for both modes: Clojure and ClojureScript_
|
||||
|
||||
Now that the theory is laid out...
|
||||
|
||||
### Mounting that ClojureScript
|
||||
|
||||
Let's look at the example [ClojureScript app](../dev/cljs/app) that uses mount to manage several states:
|
||||
|
||||
* [Datascript](https://github.com/tonsky/datascript) Database
|
||||
* Websocket Connection
|
||||
* Configuration
|
||||
|
||||
In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with:
|
||||
|
||||
```
|
||||
$ boot cljs-example
|
||||
Started Jetty on http://localhost:3000
|
||||
nREPL server started on port 64412 on host 127.0.0.1 - nrepl://127.0.0.1:64412
|
||||
Adding :require adzerk.boot-cljs-repl to mount.cljs.edn...
|
||||
Compiling ClojureScript...
|
||||
• mount.js
|
||||
```
|
||||
|
||||
And just open a browser at [http://localhost:3000](http://localhost:3000):
|
||||
|
||||
<img src="img/mount.cljs.example.png" width="700">
|
||||
|
||||
The flow behind the app is quite simple:
|
||||
|
||||
* load config
|
||||
* open a WebSocket connection
|
||||
* keep an audit log in Datascript
|
||||
* call `(mount/stop)` to disconnect
|
||||
|
||||
#### Using States
|
||||
|
||||
A good example of derefing state is here in [websockets.cljs](https://github.com/tolitius/mount/blob/0825ad2ed085b73b7ae989b4382ce4e0376e4be3/dev/cljs/app/websockets.cljs#L21):
|
||||
|
||||
```clojure
|
||||
|
||||
(ns app.websockets
|
||||
(:require [app.conf :refer [config]]
|
||||
[app.audit-log :refer [audit log]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
;; ...
|
||||
|
||||
(defstate system-a :start (connect (get-in @config [:system-a :uri]))
|
||||
:stop (disconnect system-a))
|
||||
```
|
||||
|
||||
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
|
||||
can be just passed along to the `connect` function to be `@`ed there instead.
|
||||
|
||||
### Thanks
|
||||
|
||||
I'd like to thank these good people for brainstorming and supporting the idea of Mount in ClojureScript universe:
|
||||
|
||||
[@DomKM](https://github.com/DomKM), [@yogthos](https://github.com/yogthos) and [@edvorg](https://github.com/edvorg)
|
||||
|
|
@ -16,6 +16,25 @@ While this is a common understanding, the Component is far from being Spring, in
|
|||
* it aims to solve one thing and one thing only: manage application state via inversion of control
|
||||
|
||||
The not so hidden benefit is REPL time reloadability that it brings to the table with `component/start` and `component/stop`
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
|
||||
|
||||
- [Then why "mount"!?](#then-why-mount)
|
||||
- [So what are the differences?](#so-what-are-the-differences)
|
||||
- [Component requires whole app buy in](#component-requires-whole-app-buy-in)
|
||||
- [Start and Stop Order](#start-and-stop-order)
|
||||
- [Refactoring an existing application](#refactoring-an-existing-application)
|
||||
- [Code navigation](#code-navigation)
|
||||
- [Objects vs. Namespaces](#objects-vs-namespaces)
|
||||
- [Starting and stopping parts of an application](#starting-and-stopping-parts-of-an-application)
|
||||
- [Boilerplate code](#boilerplate-code)
|
||||
- [Library vs. Framework](#library-vs-framework)
|
||||
- [What Component does better](#what-component-does-better)
|
||||
- [Multiple separate systems within the same JVM](#multiple-separate-systems-within-the-same-jvm)
|
||||
- [Visualizing dependency graph](#visualizing-dependency-graph)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Then why "mount"!?
|
||||
|
||||
|
|
@ -25,34 +44,18 @@ While Component is an interesting way to manage state, it has its limitations th
|
|||
from having the ultimate super power of Clojure: _fun working with it_. Plus several other disadvantages
|
||||
that we wanted to "fix".
|
||||
|
||||
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:
|
||||
|
||||
> 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.
|
||||
|
||||
## So what are the differences?
|
||||
|
||||
### Objects vs. Namespaces
|
||||
|
||||
One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything.
|
||||
This is how Component "separates explicit dependencies" and "clears the bounaries".
|
||||
|
||||
This is also how an Object Oriented language does it, which does not leave a lot of room for functions:
|
||||
with Component most of the functions are _methods_ which is an important distinction.
|
||||
|
||||
Mount relies on Clojure namespaces to clear the boundaries. No change from Clojure here: `defstate` in one namespace
|
||||
can be easily `:require`d in another.
|
||||
|
||||
### Start and Stop Order
|
||||
|
||||
Component relies on a cool [dependency](https://github.com/stuartsierra/dependency) library to build
|
||||
a graph of dependencies, and start/stop them via topological sort based on the dependencies in this graph.
|
||||
|
||||
Since Mount relies on Clojure namespaces and `:require`/`:use`, the order of states
|
||||
and their dependencies are revealed by the Clojure Compiler itself. Mount just records that order and replays
|
||||
it back and forth on stop and start.
|
||||
|
||||
### Component requires whole app buy in
|
||||
|
||||
Component really only works if you build your entire app around its model: application is fully based on Components
|
||||
where every Component is an Object.
|
||||
|
||||
Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/whereever
|
||||
Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/wherever
|
||||
you need it and use it.
|
||||
|
||||
This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "_open_" application
|
||||
|
|
@ -65,6 +68,15 @@ than to
|
|||
|
||||
Again this is mostly a personal preference: the code works in both cases.
|
||||
|
||||
### Start and Stop Order
|
||||
|
||||
Component relies on a cool [dependency](https://github.com/stuartsierra/dependency) library to build
|
||||
a graph of dependencies, and start/stop them via topological sort based on the dependencies in this graph.
|
||||
|
||||
Since Mount relies on Clojure namespaces and `:require`/`:use`, the order of states
|
||||
and their dependencies are revealed by the Clojure Compiler itself. Mount just records that order and replays
|
||||
it back and forth on stop and start.
|
||||
|
||||
### Refactoring an existing application
|
||||
|
||||
Since to get the most benefits of Component the approach is "all or nothing", to rewrite an existing application
|
||||
|
|
@ -72,14 +84,23 @@ in Component, depending on the application size, is daunting at best.
|
|||
|
||||
Mount allows adding `defstates` _incrementally_, the same way you would add functions to an application.
|
||||
|
||||
### Code navigation (vi, emacs, IDE..)
|
||||
### Code navigation
|
||||
|
||||
Navigation between functions in Component can't really be done without Components themselves. Since in Component
|
||||
a function usually references another function via a map lookup: `(:function component)`. This is not a big deal, but
|
||||
it changes the way IDE / editors are used to navigate the code by adding that extra step.
|
||||
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)
|
||||
|
||||
Since Mount relies on Clojure namespaces and `:require`/`:use`, the navigation accorss functions / states is exactly
|
||||
the same with or without Mount: there are no extra click/mental steps.
|
||||
Since Mount relies on Clojure namespaces (`:require`/`:use`), navigation across functions / states is exactly
|
||||
the same with or without Mount: there are no extra mental steps.
|
||||
|
||||
### Objects vs. Namespaces
|
||||
|
||||
One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything.
|
||||
This is how Component "separates explicit dependencies" and "clears the boundaries".
|
||||
|
||||
This is also how an Object Oriented language does it, which does not leave a lot of room for functions:
|
||||
with Component most of the functions are _methods_ which is an important distinction.
|
||||
|
||||
Mount relies on Clojure namespaces to clear the boundaries. No change from Clojure here: `defstate` in one namespace
|
||||
can be easily `:require`d in another.
|
||||
|
||||
### Starting and stopping _parts_ of an application
|
||||
|
||||
|
|
@ -98,6 +119,8 @@ dev=> (mount/start #'app.config/app-config #'app.nyse/conn)
|
|||
dev=>
|
||||
```
|
||||
|
||||
Here is more [documentation](../README.md#start-and-stop-parts-of-application) on how to start/stop parts of an app.
|
||||
|
||||
### Boilerplate code
|
||||
|
||||
Component does not require a whole lot of "extra" code but:
|
||||
|
|
@ -113,36 +136,47 @@ Depending on the number of application components the "extra" size may vary.
|
|||
Mount is pretty much:
|
||||
|
||||
```clojure
|
||||
(defstate name :start (fn)
|
||||
:stop (fn))
|
||||
(defstate name :start fn
|
||||
:stop fn)
|
||||
```
|
||||
|
||||
no "ceremony".
|
||||
|
||||
### Library vs. Framework
|
||||
|
||||
Mount uses namespaces and vars where Component uses records and protocols.
|
||||
|
||||
Component manages records and protocols,
|
||||
and in order to do that it requires a whole app buy-in,
|
||||
which makes it a _framework_.
|
||||
|
||||
Mount does not need to manage namespaces and vars,
|
||||
since it is very well managed by the Clojure Compiler,
|
||||
which makes it a _library_.
|
||||
|
||||
## What Component does better
|
||||
|
||||
### Swapping alternate implementations
|
||||
### Multiple separate systems within the same JVM
|
||||
|
||||
This is someting that is very useful for testing and is very easy to do in Component by simply assoc'ing onto a map.
|
||||
In Mount you can redef the state, but it is not as elegant and decoupled as it is in Component.
|
||||
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_.
|
||||
|
||||
###### _conclusion: needs more thinking._
|
||||
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.
|
||||
|
||||
### Uberjar / Packaging
|
||||
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.
|
||||
|
||||
Since Component fully controls the `system` where the whole application lives, it is quite simple
|
||||
to start an application from anywhere including a `-main` function of the uberjar.
|
||||
Testing is not alien to Mount and it knows how to do a thing or two:
|
||||
|
||||
In order to start the whole system in development, Mount just needs `(mount/start)` or `(reset)`
|
||||
it's [simple](https://github.com/tolitius/mount#the-importance-of-being-reloadable).
|
||||
* [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)
|
||||
* [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states)
|
||||
* [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations)
|
||||
* [stop an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states)
|
||||
* [composing states](https://github.com/tolitius/mount#composing-states)
|
||||
|
||||
However there is no "tools.namespaces"/REPL at a "stand alone jar runtime" and in order for Mount to start / stop
|
||||
the app, states need to be `:require`/`:use`d, which is usually done within the same namespace as `-main`.
|
||||
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).
|
||||
|
||||
Depending on app dependencies, it could only require a few states to be `:require`/`:use`d, others
|
||||
will be brought transitively.
|
||||
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.
|
||||
|
||||
###### _conclusion: it's simple in Mount as well, but requires an additional step._
|
||||
###### _conclusion: can be done with mount as well, but via a different dependency._
|
||||
|
||||
### Visualizing dependency graph
|
||||
|
||||
|
|
@ -151,3 +185,31 @@ Having this visualization is really helpful, especially during code discusions b
|
|||
|
||||
Mount does not have this at the moment. It does have all the data to create such a visualization, perhaps even
|
||||
by building a graph out of the data it has just for this purpose.
|
||||
|
||||
There is a [`(states-with-deps)`](https://github.com/tolitius/mount/blob/master/src/mount/tools/graph.cljc#L20) function that can help out:
|
||||
|
||||
```clojure
|
||||
dev=> (require '[mount.tools.graph :as graph])
|
||||
|
||||
dev=> (graph/states-with-deps)
|
||||
({:name "#'app.conf/config",
|
||||
:order 1,
|
||||
:status #{:started},
|
||||
:deps #{}}
|
||||
{:name "#'app.db/conn",
|
||||
:order 2,
|
||||
:status #{:started},
|
||||
:deps #{"#'app.conf/config"}}
|
||||
{:name "#'app.www/nyse-app",
|
||||
:order 3,
|
||||
:status #{:started},
|
||||
:deps #{"#'app.conf/config"}}
|
||||
{:name "#'app.example/nrepl",
|
||||
:order 4,
|
||||
:status #{:started},
|
||||
:deps #{"#'app.www/nyse-app" "#'app.conf/config"}})
|
||||
```
|
||||
|
||||
But it does not draw :), and it currently only supports Clojure, not ClojureScript.
|
||||
|
||||
###### _conclusion: needs more thinking._
|
||||
|
|
|
|||
BIN
doc/img/cljs-ns-reload.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
BIN
doc/img/mount-logo.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
doc/img/mount.cljs.example.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
doc/img/ns-recompile.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
BIN
doc/img/slack-icon.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
# Introduction to statuo
|
||||
|
||||
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)
|
||||
130
doc/runtime-arguments.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
## Passing Runtime Arguments
|
||||
|
||||
This example lives in the `with-args` branch. If you'd like to follow along:
|
||||
|
||||
```bash
|
||||
$ git checkout with-args
|
||||
Switched to branch 'with-args'
|
||||
```
|
||||
|
||||
## Start with args
|
||||
|
||||
In order to pass runtime arguments, these could be `-this x -that y` params or `-Dparam=` or
|
||||
just a path to an external configuration file, `mount` has a special `start-with-args` function:
|
||||
|
||||
```clojure
|
||||
(defn -main [& args]
|
||||
(mount/start-with-args args))
|
||||
```
|
||||
|
||||
Most of the time it is better to parse args before they "get in", so usually accepting args would look something like:
|
||||
|
||||
```clojure
|
||||
(defn -main [& args]
|
||||
(mount/start-with-args
|
||||
(parse-args args)))
|
||||
```
|
||||
|
||||
where the `parse-args` is an app specific function.
|
||||
|
||||
### Reading arguments
|
||||
|
||||
Once the arguments are passed to the app, they are available via:
|
||||
|
||||
```clojure
|
||||
(mount/args)
|
||||
```
|
||||
|
||||
Which, unless the arguments were parsed or modified in the `-main` function,
|
||||
will return the original `args` that were passed to `-main`.
|
||||
|
||||
### "Reading" example
|
||||
|
||||
Here is an [example app](https://github.com/tolitius/mount/blob/with-args/test/app/app.clj) that takes `-main` arguments
|
||||
and parses them with [tools.cli](https://github.com/clojure/tools.cli):
|
||||
|
||||
```clojure
|
||||
;; "any" regular function to pass arguments
|
||||
(defn parse-args [args]
|
||||
(let [opts [["-d" "--datomic-uri [datomic url]" "Datomic URL"
|
||||
:default "datomic:mem://mount"]
|
||||
["-h" "--help"]]]
|
||||
(-> (parse-opts args opts)
|
||||
:options)))
|
||||
|
||||
;; example of an app entry point with arguments
|
||||
(defn -main [& args]
|
||||
(mount/start-with-args
|
||||
(parse-args args)))
|
||||
```
|
||||
|
||||
For the example sake the app reads arguments in two places:
|
||||
|
||||
* [inside](https://github.com/tolitius/mount/blob/with-args/test/app/nyse.clj#L17) a `defstate`
|
||||
|
||||
```clojure
|
||||
(defstate conn :start (new-connection (mount/args))
|
||||
:stop (disconnect (mount/args) conn))
|
||||
```
|
||||
|
||||
* and from "any" [other place](https://github.com/tolitius/mount/blob/with-args/test/app/config.clj#L8) within a function:
|
||||
|
||||
```clojure
|
||||
(defn load-config [path]
|
||||
;; ...
|
||||
(if (:help (mount/args))
|
||||
(info "\n\nthis is a sample mount app to demo how to pass and read runtime arguments\n"))
|
||||
;; ...)
|
||||
```
|
||||
|
||||
### "Uber" example
|
||||
|
||||
In order to demo all of the above, we'll build an uberjar:
|
||||
|
||||
```bash
|
||||
$ lein do clean, uberjar
|
||||
...
|
||||
Created .. mount/target/mount-0.1.5-SNAPSHOT-standalone.jar
|
||||
```
|
||||
|
||||
Since we have a default for a Datomic URI, it'll work with no arguments:
|
||||
|
||||
```bash
|
||||
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar
|
||||
|
||||
22:12:03.290 [main] INFO mount - >> starting.. app-config
|
||||
22:12:03.293 [main] INFO mount - >> starting.. conn
|
||||
22:12:03.293 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount
|
||||
22:12:03.444 [main] INFO mount - >> starting.. nrepl
|
||||
```
|
||||
|
||||
Now let's ask it to help us:
|
||||
|
||||
```bash
|
||||
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar --help
|
||||
|
||||
22:13:48.798 [main] INFO mount - >> starting.. app-config
|
||||
22:13:48.799 [main] INFO app.config -
|
||||
|
||||
this is a sample mount app to demo how to pass and read runtime arguments
|
||||
|
||||
22:13:48.801 [main] INFO mount - >> starting.. conn
|
||||
22:13:48.801 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount
|
||||
22:13:48.946 [main] INFO mount - >> starting.. nrepl
|
||||
```
|
||||
|
||||
And finally let's connect to the Single Malt Database. It's Friday..
|
||||
|
||||
```bash
|
||||
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar -d datomic:mem://single-malt-database
|
||||
|
||||
22:16:10.733 [main] INFO mount - >> starting.. app-config
|
||||
22:16:10.737 [main] INFO mount - >> starting.. conn
|
||||
22:16:10.737 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://single-malt-database
|
||||
22:16:10.885 [main] INFO mount - >> starting.. nrepl
|
||||
```
|
||||
|
||||
### Other usecases
|
||||
|
||||
Depending the requirements, these runtime arguments could take different shapes of forms. You would have a full control
|
||||
over what is passed to the app, the same way you have it without mount through `-main [& args]`.
|
||||
|
|
@ -12,10 +12,10 @@ Switched to branch 'uberjar'
|
|||
Here is an example [app](https://github.com/tolitius/mount/tree/uberjar/test/app) that has these states:
|
||||
|
||||
```clojure
|
||||
16:20:44.997 [nREPL-worker-0] INFO mount - >> starting.. app-config
|
||||
16:20:44.998 [nREPL-worker-0] INFO mount - >> starting.. conn
|
||||
16:20:45.393 [nREPL-worker-0] INFO mount - >> starting.. nyse-app
|
||||
16:20:45.443 [nREPL-worker-0] INFO mount - >> starting.. nrepl
|
||||
22:15:17.175 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.config/app-config
|
||||
22:15:17.176 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
22:15:17.196 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
22:15:17.199 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app/nrepl
|
||||
```
|
||||
|
||||
where `nyse-app` is _the_ app. It has the usual routes:
|
||||
|
|
@ -38,14 +38,13 @@ where `nyse-app` is _the_ app. It has the usual routes:
|
|||
and the reloadable state:
|
||||
|
||||
```clojure
|
||||
(defn start-nyse []
|
||||
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
|
||||
(defn start-nyse [{:keys [www]}]
|
||||
(-> (routes mount-example-routes)
|
||||
(handler/site)
|
||||
(run-jetty {:join? false
|
||||
:port (get-in app-config [:www :port])})))
|
||||
:port (:port www)})))
|
||||
|
||||
(defstate nyse-app :start (start-nyse)
|
||||
(defstate nyse-app :start (start-nyse app-config)
|
||||
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
|
||||
```
|
||||
|
||||
|
|
@ -67,13 +66,8 @@ And some usual suspects from `project.clj`:
|
|||
```clojure
|
||||
;; "test" is in sources here to just "demo" the uberjar without poluting mount "src"
|
||||
:uberjar {:source-paths ["test/app"]
|
||||
:dependencies [[compojure "1.4.0"]
|
||||
[ring/ring-jetty-adapter "1.1.0"]
|
||||
[cheshire "5.5.0"]
|
||||
[org.clojure/tools.nrepl "0.2.11"]
|
||||
[com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]
|
||||
:main app
|
||||
:aot :all}}
|
||||
:aot :all}})
|
||||
```
|
||||
|
||||
### REPL time
|
||||
|
|
@ -82,60 +76,56 @@ And some usual suspects from `project.clj`:
|
|||
$ lein do clean, repl
|
||||
|
||||
user=> (dev)(reset)
|
||||
16:20:44.997 [nREPL-worker-0] INFO mount - >> starting.. app-config
|
||||
16:20:44.998 [nREPL-worker-0] INFO mount - >> starting.. conn
|
||||
16:20:45.393 [nREPL-worker-0] INFO mount - >> starting.. nyse-app
|
||||
|
||||
16:20:45.442 [nREPL-worker-0] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:53600
|
||||
|
||||
16:20:45.443 [nREPL-worker-0] INFO mount - >> starting.. nrepl
|
||||
:ready
|
||||
22:15:17.175 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.config/app-config
|
||||
22:15:17.176 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
22:15:17.196 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app.www/nyse-app
|
||||
22:15:17.199 [nREPL-worker-0] INFO app.utils.logging - >> starting.. #'app/nrepl
|
||||
dev=>
|
||||
```
|
||||
|
||||
Jetty server is started and ready to roll. And everything is still restartable:
|
||||
Jetty server is started and ready to roll. And everything is still reloadable:
|
||||
|
||||
```clojure
|
||||
user=> (reset)
|
||||
16:44:16.625 [nREPL-worker-2] INFO mount - << stopping.. nrepl
|
||||
16:44:16.626 [nREPL-worker-2] INFO mount - << stopping.. nyse-app
|
||||
16:44:16.711 [nREPL-worker-2] INFO mount - << stopping.. conn
|
||||
16:44:16.713 [nREPL-worker-2] INFO mount - << stopping.. app-config
|
||||
dev=> (reset)
|
||||
22:19:49.436 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app/nrepl
|
||||
22:19:49.436 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app.db/conn
|
||||
22:19:49.437 [nREPL-worker-3] INFO app.utils.logging - << stopping.. #'app.config/app-config
|
||||
|
||||
16:44:16.747 [nREPL-worker-2] INFO mount - >> starting.. app-config
|
||||
16:44:16.748 [nREPL-worker-2] INFO mount - >> starting.. conn
|
||||
16:44:16.773 [nREPL-worker-2] INFO mount - >> starting.. nyse-app
|
||||
:reloading ()
|
||||
|
||||
16:44:16.777 [nREPL-worker-2] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:54476
|
||||
|
||||
16:44:16.778 [nREPL-worker-2] INFO mount - >> starting.. nrepl
|
||||
22:19:49.471 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app.config/app-config
|
||||
22:19:49.472 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app.db/conn
|
||||
22:19:49.490 [nREPL-worker-3] INFO app.utils.logging - >> starting.. #'app/nrepl
|
||||
```
|
||||
|
||||
Notice the Jetty port difference between reloads: `53600` vs. `54476`. This is done on purpose via [config](https://github.com/tolitius/mount/blob/uberjar/test/resources/config.edn#L4):
|
||||
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):
|
||||
|
||||
```clojure
|
||||
:www {:port 0} ;; Jetty will randomly assign the available port (this is good for dev reloadability)
|
||||
(defn stop []
|
||||
(mount/stop-except #'app.www/nyse-app))
|
||||
```
|
||||
|
||||
This of course can be solidified for different env deployments. For example I like `4242` :)
|
||||
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).
|
||||
|
||||
### Packaging one super uber jar
|
||||
|
||||
```clojure
|
||||
$ lein do clean, uberjar
|
||||
...
|
||||
Created /Users/tolitius/1/fun/mount/target/mount-0.1.0-SNAPSHOT-standalone.jar ;; your version may vary
|
||||
Created /path-to/mount/target/mount-0.1.5-SNAPSHOT-standalone.jar ;; your version may vary
|
||||
```
|
||||
|
||||
Let's give it a spin:
|
||||
|
||||
```bash
|
||||
$ java -jar target/mount-0.1.0-SNAPSHOT-standalone.jar
|
||||
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar
|
||||
...
|
||||
16:51:35.586 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:54728
|
||||
22:25:35.303 [main] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:4242
|
||||
22:25:35.303 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:4242
|
||||
22:25:35.304 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED org.eclipse.jetty.server.Server@ab2009f
|
||||
```
|
||||
|
||||
Up and running on port `:54728`:
|
||||
Up and running on port `:4242`:
|
||||
|
||||
<img src="img/welcome-uberjar.png" width="250">
|
||||
|
||||
|
|
@ -146,7 +136,7 @@ See if we have any orders:
|
|||
we don't. let's put something into Datomic:
|
||||
|
||||
```clojure
|
||||
$ curl -X POST -d "ticker=GOOG&qty=100&bid=665.51&offer=665.59" "http://localhost:54728/nyse/orders"
|
||||
$ curl -X POST -d "ticker=GOOG&qty=100&bid=665.51&offer=665.59" "http://localhost:4242/nyse/orders"
|
||||
{"added":{"ticker":"GOOG","qty":"100","bid":"665.51","offer":"665.59"}}
|
||||
```
|
||||
|
||||
|
|
|
|||
23
package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@tolitius/mount",
|
||||
"version": "0.1.16",
|
||||
"license": "EPL-1.0",
|
||||
"homepage": "https://github.com/tolitius/mount",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tolitius/mount"
|
||||
},
|
||||
"author": {
|
||||
"name": "tolitius",
|
||||
"url": "http://www.dotkam.com"
|
||||
},
|
||||
"files": [
|
||||
"src/*"
|
||||
],
|
||||
"directories": {
|
||||
"lib": "src"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
45
pom.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<groupId>mount</groupId>
|
||||
<artifactId>mount</artifactId>
|
||||
<version>0.1.23</version>
|
||||
<name>mount</name>
|
||||
<description>managing Clojure and ClojureScript app state since (reset)</description>
|
||||
<url>https://github.com/tolitius/mount</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Eclipse Public License</name>
|
||||
<url>http://www.eclipse.org/legal/epl-v10.html</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>tolitius</name>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<url>https://github.com/tolitius/mount</url>
|
||||
<connection>scm:git:git://github.com/tolitius/mount.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/tolitius/mount.git</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.clojure</groupId>
|
||||
<artifactId>clojure</artifactId>
|
||||
<version>1.11.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>clojars</id>
|
||||
<url>https://repo.clojars.org/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
70
project.clj
|
|
@ -1,18 +1,68 @@
|
|||
(defproject mount "0.2.0-SNAPSHOT"
|
||||
:description "managing Clojure app state since (reset)"
|
||||
(defproject mount "0.1.17"
|
||||
:description "managing Clojure and ClojureScript app state since (reset)"
|
||||
:url "https://github.com/tolitius/mount"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
|
||||
:source-paths ["src" "src/mount"]
|
||||
:source-paths ["src"]
|
||||
|
||||
:dependencies [[org.clojure/clojure "1.7.0"]
|
||||
:dependencies [] ;; for visual clarity
|
||||
|
||||
:tach {:test-runner-ns 'mount.test-self-host
|
||||
:source-paths ["test/core"]}
|
||||
|
||||
:profiles {:dev {:source-paths ["dev" "dev/clj" "test/clj"]
|
||||
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||
[org.clojure/clojurescript "1.9.946"]; :classifier "aot"]
|
||||
[datascript "0.13.3"]
|
||||
[compojure "1.4.0"]
|
||||
[ring/ring-jetty-adapter "1.1.0"]
|
||||
[cheshire "5.5.0"]
|
||||
[hiccups "0.3.0"]
|
||||
[com.andrewmcveigh/cljs-time "0.3.14"]
|
||||
[ch.qos.logback/logback-classic "1.1.3"]
|
||||
[org.clojure/tools.logging "0.3.1"]
|
||||
[org.clojure/tools.macro "0.1.2"]
|
||||
[org.clojure/tools.namespace "0.2.11"]]
|
||||
|
||||
:profiles {:dev {:source-paths ["dev" "test/app"]
|
||||
:dependencies [[yesql "0.5.1"]
|
||||
[robert/hooke "1.3.0"]
|
||||
[org.clojure/tools.namespace "0.2.11"]
|
||||
[org.clojure/tools.nrepl "0.2.11"]
|
||||
[com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]}})
|
||||
[com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]
|
||||
|
||||
:plugins [[lein-cljsbuild "1.1.1"]
|
||||
[lein-doo "0.1.6"]
|
||||
[lein-figwheel "0.5.0-2"]
|
||||
[test2junit "1.1.3"]
|
||||
[lein-tach "1.0.0"]]
|
||||
|
||||
:test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit")
|
||||
|
||||
:clean-targets ^{:protect false} [:target-path
|
||||
[:cljsbuild :builds :dev :compiler :output-dir]
|
||||
[:cljsbuild :builds :prod :compiler :output-to]]
|
||||
:cljsbuild {
|
||||
:builds {:dev
|
||||
{:source-paths ["src" "dev/cljs"]
|
||||
:figwheel true
|
||||
|
||||
:compiler {:main app.example
|
||||
:asset-path "js/compiled/out"
|
||||
:output-to "dev/resources/public/js/compiled/mount.js"
|
||||
:output-dir "dev/resources/public/js/compiled/out"
|
||||
:optimizations :none
|
||||
:source-map true
|
||||
:source-map-timestamp true}}
|
||||
:test
|
||||
{:source-paths ["src" "dev/cljs" "test"]
|
||||
:compiler {:main mount.test
|
||||
;; :asset-path "js/compiled/out"
|
||||
:output-to "dev/resources/public/js/compiled/mount.js"
|
||||
:output-dir "dev/resources/public/js/compiled/test"
|
||||
:optimizations :none
|
||||
:source-map true
|
||||
:source-map-timestamp true}}
|
||||
:prod
|
||||
{:source-paths ["src" "dev/cljs"]
|
||||
:compiler {:output-to "dev/resources/public/js/compiled/mount.js"
|
||||
:optimizations :advanced
|
||||
:pretty-print false}}}}}
|
||||
|
||||
:test {:source-paths ["test/core" "test/clj" "test/cljs"]}})
|
||||
|
|
|
|||
3
resources/clj-kondo.exports/mount/mount/config.edn
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{:linters {:mount/defstate {:level :warning}}
|
||||
:hooks {:analyze-call {mount.core/defstate hooks.defstate/defstate
|
||||
mount.core/defstate! hooks.defstate/defstate}}}
|
||||
39
resources/clj-kondo.exports/mount/mount/hooks/defstate.clj
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
(ns hooks.defstate
|
||||
(:require [clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn defstate [{:keys [node]}]
|
||||
(let [[n & args] (next (:children node))
|
||||
[docs args] (if (string? (api/sexpr (first args)))
|
||||
[(first args) (next args)]
|
||||
[nil args])
|
||||
m (when-let [m (first (:meta n))]
|
||||
(api/sexpr m))
|
||||
m (if (map? m) m {})
|
||||
ks (cond-> (take 1 args)
|
||||
(> (count args) 2) (conj (nth args 2)))
|
||||
invalid-key (first (remove (comp (partial contains? #{:start :stop}) api/sexpr) ks))]
|
||||
(cond
|
||||
invalid-key
|
||||
(api/reg-finding!
|
||||
{:message (str "lifecycle functions can only contain `:start` and `:stop`. illegal function found: " (api/sexpr invalid-key))
|
||||
:type :mount/defstate
|
||||
:row (:row (meta invalid-key))
|
||||
:col (:col (meta invalid-key))})
|
||||
(not (contains? (set (map api/sexpr ks)) :start))
|
||||
(throw (ex-info "lifecycle functions must include `:start`" {}))
|
||||
((complement contains?) #{2 4} (count args))
|
||||
(throw (ex-info "lifecycle functions must consist of no more than 2 pair forms: `:start` and `:stop`" {}))
|
||||
(and (contains? m :on-reload) (not (contains? #{:noop :stop} (:on-reload m))))
|
||||
(api/reg-finding!
|
||||
{:message "metadata `:on-reload` key can only have value of `noop` or `stop`"
|
||||
:type :mount/defstate
|
||||
:row (:row (meta n))
|
||||
:col (:col (meta n))})
|
||||
:else
|
||||
{:node (api/list-node
|
||||
(cond-> [(api/token-node 'def) n]
|
||||
docs (conj docs)
|
||||
true (conj (api/list-node
|
||||
(list*
|
||||
(api/token-node 'do)
|
||||
args)))))})))
|
||||
411
src/mount/core.cljc
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
(ns mount.core
|
||||
#?(:clj {:clojure.tools.namespace.repl/load false}) ; prevent reloading of this ns
|
||||
#?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro]
|
||||
[mount.tools.macrovich :refer [deftime]]
|
||||
[mount.tools.logger :refer [log]]
|
||||
[clojure.set :refer [intersection]]
|
||||
[clojure.string :as s])
|
||||
:cljs (:require [mount.tools.macro]
|
||||
[clojure.set :refer [intersection]]
|
||||
[mount.tools.logger :refer [log]]))
|
||||
#?(:cljs (:require-macros [mount.core]
|
||||
[mount.tools.macro :refer [on-error throw-runtime]]
|
||||
[mount.tools.macrovich :refer [deftime]])))
|
||||
|
||||
(defonce ^:private -args (atom {})) ;; mostly for command line args and external files
|
||||
(defonce ^:private state-seq (atom 0))
|
||||
(defonce ^:private mode (atom :clj))
|
||||
(defonce ^:private meta-state (atom {}))
|
||||
(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
|
||||
|
||||
(defn- make-state-seq [state]
|
||||
(or (:order (@meta-state state))
|
||||
(swap! state-seq inc)))
|
||||
|
||||
(deftype NotStartedState [state]
|
||||
Object
|
||||
(toString [this]
|
||||
(str "'" state "' is not started (to start all the states call mount/start)")))
|
||||
|
||||
;;TODO validate the whole lifecycle
|
||||
(defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
|
||||
(cond
|
||||
(not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)")
|
||||
(or suspend resume) (throw-runtime "suspend / resume lifecycle support was removed in \"0.1.10\" in favor of (mount/stop-except)")))
|
||||
|
||||
(defn- with-ns [ns name]
|
||||
(str "#'" ns "/" name))
|
||||
|
||||
(defn- pounded? [f]
|
||||
(let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
|
||||
(.startsWith (str f) pound)))
|
||||
|
||||
(defn unpound [f]
|
||||
(if (pounded? f)
|
||||
(nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
|
||||
f))
|
||||
|
||||
(defn cleanup-if-dirty
|
||||
"in case a namespace is recompiled without calling (mount/stop),
|
||||
a running state instance will still be running.
|
||||
this function stops this 'lost' state instance.
|
||||
it is meant to be called by defstate before defining a new state"
|
||||
[state reason]
|
||||
(when-let [{:keys [stop] :as up} (@running state)]
|
||||
(when stop
|
||||
(log (str "<< stopping.. " state " " reason))
|
||||
(stop))
|
||||
(swap! running dissoc state)))
|
||||
|
||||
#?(:clj
|
||||
(defn alter-state! [{:keys [var inst]} value]
|
||||
(if (= @mode :cljc)
|
||||
(reset! inst value)
|
||||
(alter-var-root var (constantly value))))
|
||||
|
||||
:cljs
|
||||
(defn alter-state! [{:keys [inst]} value]
|
||||
(reset! inst value)))
|
||||
|
||||
(defn- update-meta! [path v]
|
||||
(swap! meta-state assoc-in path v))
|
||||
|
||||
(defn- record! [state-name f done]
|
||||
(let [state (f)]
|
||||
(swap! done conj state-name)
|
||||
state))
|
||||
|
||||
(defn- up [state {:keys [start stop status] :as current} done]
|
||||
(when-not (:started status)
|
||||
(let [s (on-error (str "could not start [" state "] due to")
|
||||
(record! state start done))]
|
||||
(alter-state! current s)
|
||||
(swap! running assoc state {:stop stop})
|
||||
(update-meta! [state :status] #{:started}))))
|
||||
|
||||
(defn- down
|
||||
"brings a state down by
|
||||
* calling its 'stop' function if it is defined
|
||||
* if not defined, state will still become a 'NotStartedState'
|
||||
* in case of a failure on 'stop', state is still marked as :stopped, and the error is logged / printed
|
||||
* dissoc'ing it from the running states
|
||||
* marking it as :stopped"
|
||||
[state {:keys [stop status] :as current} done]
|
||||
(when (some status #{:started})
|
||||
(if stop
|
||||
(if-let [cause (-> (on-error (str "could not stop [" state "] due to")
|
||||
(record! state stop done)
|
||||
:fail? false)
|
||||
:f-failed)]
|
||||
(log cause :error) ;; this would mostly be useful in REPL / browser console
|
||||
(alter-state! current (->NotStartedState state)))
|
||||
(alter-state! current (->NotStartedState state))) ;; (!) if a state does not have :stop when _should_ this might leak
|
||||
(swap! running dissoc state)
|
||||
(update-meta! [state :status] #{:stopped})))
|
||||
|
||||
(defn running-states []
|
||||
(set (keys @running)))
|
||||
|
||||
(deftype DerefableState [name]
|
||||
#?(:clj clojure.lang.IDeref
|
||||
:cljs IDeref)
|
||||
(#?(:clj deref
|
||||
:cljs -deref)
|
||||
[_]
|
||||
(let [{:keys [status var inst] :as state} (@meta-state name)]
|
||||
(when-not (:started status)
|
||||
(if (= :throw (-> var meta :on-lazy-start))
|
||||
(throw-runtime (str ":on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} " name "...) "
|
||||
"and " name " state was not explicitly started before it was deref'ed (i.e. @" name ")"))
|
||||
(up name state (atom #{}))))
|
||||
@inst))
|
||||
#?(:clj clojure.lang.IPending
|
||||
:cljs IPending)
|
||||
(#?(:clj isRealized
|
||||
:cljs -realized?)
|
||||
[_]
|
||||
(boolean ((running-states) name))))
|
||||
|
||||
#?(:clj
|
||||
(defn current-state [state]
|
||||
(let [{:keys [var]} (@meta-state state)]
|
||||
(if (= @mode :cljc)
|
||||
(->DerefableState state)
|
||||
(var-get var))))
|
||||
|
||||
:cljs
|
||||
(defn current-state [state]
|
||||
(-> (@meta-state state) :inst deref)))
|
||||
|
||||
(defn on-reload-meta [s-var]
|
||||
(or (-> s-var meta :on-reload)
|
||||
:restart)) ;; restart by default on ns reload
|
||||
|
||||
(defn running-noop? [s-name]
|
||||
(let [{:keys [var status]} (@meta-state s-name)
|
||||
on-reload (-> var meta :on-reload)]
|
||||
(when status
|
||||
(and (status :started)
|
||||
(= :noop on-reload)))))
|
||||
|
||||
;;TODO: make private after figuring out the inconsistency betwen cljs compile stages
|
||||
;; (i.e. _sometimes_ this, if private, is not seen by expanded "defmacro" on cljs side)
|
||||
(defn mount-it [s-var s-name s-meta]
|
||||
(let [with-inst (assoc s-meta :inst (atom (->NotStartedState s-name))
|
||||
:var s-var)
|
||||
on-reload (on-reload-meta s-var)
|
||||
existing? (when-not (= :noop on-reload)
|
||||
(cleanup-if-dirty s-name "(namespace was recompiled)"))]
|
||||
(update-meta! [s-name] with-inst)
|
||||
(when (and existing? (= :restart on-reload))
|
||||
(log (str ">> starting.. " s-name " (namespace was recompiled)"))
|
||||
(up s-name with-inst (atom #{})))))
|
||||
|
||||
(deftime
|
||||
|
||||
(defmacro defstate
|
||||
"defines a state (a.k.a. a stateful component).
|
||||
restarts on recompilation.
|
||||
pass ^{:on-reload :noop} to prevent auto-restart on ns recompilation,
|
||||
or ^{:on-reload :stop} to stop on recompilation."
|
||||
[state & body]
|
||||
(let [[state params] (mount.tools.macro/name-with-attributes state body)
|
||||
{:keys [start stop] :as lifecycle} (apply hash-map params)
|
||||
state-name (with-ns *ns* state)
|
||||
order (make-state-seq state-name)]
|
||||
(validate lifecycle)
|
||||
(let [s-meta (cond-> {:order order
|
||||
:start `(fn [] ~start)
|
||||
:status #{:stopped}}
|
||||
stop (assoc :stop `(fn [] ~stop)))]
|
||||
`(do
|
||||
;; (log (str "|| mounting... " ~state-name))
|
||||
;; only create/redefine a new state iff this is not a running ^{:on-reload :noop}
|
||||
(if-not (running-noop? ~state-name)
|
||||
(do
|
||||
(~'defonce ~state (->DerefableState ~state-name))
|
||||
(mount-it (~'var ~state) ~state-name ~s-meta))
|
||||
(~'defonce ~state (current-state ~state-name)))
|
||||
(~'var ~state)))))
|
||||
|
||||
(defmacro defstate! [state & {:keys [start! stop!]}]
|
||||
(let [state-name (with-ns *ns* state)]
|
||||
`(defstate ~state
|
||||
:start (~'let [~state (mount.core/current-state ~state-name)]
|
||||
~start!)
|
||||
:stop (~'let [~state (mount.core/current-state ~state-name)]
|
||||
~stop!))))
|
||||
|
||||
)
|
||||
|
||||
(defn in-cljc-mode []
|
||||
(reset! mode :cljc))
|
||||
|
||||
(defn in-clj-mode []
|
||||
(reset! mode :clj))
|
||||
|
||||
;;TODO args might need more thinking
|
||||
(defn args [] @-args)
|
||||
|
||||
(defn find-all-states []
|
||||
(keys @meta-state))
|
||||
|
||||
#?(:clj
|
||||
(defn- var-to-str [v]
|
||||
(str v)))
|
||||
|
||||
#?(:cljs
|
||||
(defn var-to-str [v]
|
||||
(if (instance? cljs.core.Var v)
|
||||
(let [{:keys [ns name]} (meta v)]
|
||||
(with-ns ns name))
|
||||
v)))
|
||||
|
||||
(defn- unvar-state [s]
|
||||
(->> s (drop 2) (apply str))) ;; magic 2 is removing "#'" in state name
|
||||
|
||||
#?(:clj
|
||||
(defn- was-removed?
|
||||
"checks if a state was removed from a namespace"
|
||||
[state]
|
||||
(-> state unvar-state symbol resolve not)))
|
||||
|
||||
#?(:clj
|
||||
(defn cleanup-deleted [state]
|
||||
(when (was-removed? state)
|
||||
(cleanup-if-dirty state "(it was deleted)")
|
||||
(swap! meta-state dissoc state))))
|
||||
|
||||
(defn- bring [states fun order]
|
||||
(let [done (atom [])]
|
||||
(as-> states $
|
||||
(map var-to-str $)
|
||||
#?(:clj ;; needs more thking in cljs, since based on sym resolve
|
||||
(remove cleanup-deleted $))
|
||||
(select-keys @meta-state $)
|
||||
(sort-by (comp :order val) order $)
|
||||
(doseq [[k v] $] (fun k v done)))
|
||||
@done))
|
||||
|
||||
(defn- merge-lifecycles
|
||||
"merges with overriding _certain_ non existing keys.
|
||||
i.e. :stop is in a 'state', but not in a 'substitute': it should be overriden with nil
|
||||
however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
|
||||
([state sub]
|
||||
(merge-lifecycles state nil sub))
|
||||
([state origin {:keys [start stop status]}]
|
||||
(assoc state :origin origin
|
||||
:status status
|
||||
:start start :stop stop)))
|
||||
|
||||
(defn- rollback! [state]
|
||||
(let [{:keys [origin] :as sub} (@meta-state state)]
|
||||
(when origin
|
||||
(update-meta! [state] (merge-lifecycles sub origin)))))
|
||||
|
||||
(defn- substitute! [state with mode]
|
||||
(let [lifecycle-fns #(select-keys % [:start :stop :status])
|
||||
origin (@meta-state state)
|
||||
sub (if (= :value mode)
|
||||
{:start (fn [] with) :status :stopped}
|
||||
(assoc with :status :stopped))]
|
||||
(update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub))))
|
||||
|
||||
(defn- unsub [state]
|
||||
(when (-> (@meta-state state) :sub?)
|
||||
(update-meta! [state :sub?] nil)))
|
||||
|
||||
(defn- all-without-subs []
|
||||
(remove (comp :sub? @meta-state) (find-all-states)))
|
||||
|
||||
(defn start [& states]
|
||||
(let [fs (-> states first)]
|
||||
(if (coll? fs)
|
||||
(if-not (empty? fs) ;; (mount/start) vs. (mount/start #{}) vs. (mount/start #{1 2 3})
|
||||
(apply start fs)
|
||||
{:started #{}})
|
||||
(let [states (or (->> states (map var-to-str) seq)
|
||||
(all-without-subs))]
|
||||
{:started (bring states up <)}))))
|
||||
|
||||
(defn stop [& states]
|
||||
(let [fs (-> states first)]
|
||||
(if (coll? fs)
|
||||
(if-not (empty? fs) ;; (mount/stop) vs. (mount/stop #{}) vs. (mount/stop #{1 2 3})
|
||||
(apply stop fs)
|
||||
{:stopped #{}})
|
||||
(let [states (or (->> states (map var-to-str) seq)
|
||||
(find-all-states))
|
||||
_ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with" / "swap-states"
|
||||
stopped (bring states down >)]
|
||||
(dorun (map rollback! states)) ;; restore to origin from "start-with" / "swap-states"
|
||||
{:stopped stopped}))))
|
||||
|
||||
;; composable set of states
|
||||
|
||||
(defn- mapset [f xs]
|
||||
(-> (map f xs)
|
||||
set))
|
||||
|
||||
(defn only
|
||||
([these]
|
||||
(only (find-all-states) these))
|
||||
([states these]
|
||||
(intersection (mapset var-to-str these)
|
||||
(mapset var-to-str states))))
|
||||
|
||||
(defn with-args
|
||||
([args]
|
||||
(with-args (find-all-states) args))
|
||||
([states args]
|
||||
(reset! -args args) ;; TODO localize
|
||||
states))
|
||||
|
||||
(defn except
|
||||
([these]
|
||||
(except (find-all-states) these))
|
||||
([states these]
|
||||
(remove (mapset var-to-str these)
|
||||
(mapset var-to-str states))))
|
||||
|
||||
(defn swap
|
||||
([with]
|
||||
(swap (find-all-states) with))
|
||||
([states with]
|
||||
(doseq [[from to] with]
|
||||
(substitute! (var-to-str from)
|
||||
to :value))
|
||||
states))
|
||||
|
||||
(defn swap-states
|
||||
([with]
|
||||
(swap-states (find-all-states) with))
|
||||
([states with]
|
||||
(doseq [[from to] with]
|
||||
(substitute! (var-to-str from)
|
||||
to :state))
|
||||
states))
|
||||
|
||||
;; restart on events
|
||||
|
||||
(defprotocol ChangeListener
|
||||
(add-watcher [this ks watcher])
|
||||
(on-change [this k]))
|
||||
|
||||
(deftype RestartListener [watchers]
|
||||
ChangeListener
|
||||
|
||||
(add-watcher [_ ks state]
|
||||
(doseq [k ks]
|
||||
(swap! watchers update k (fn [v]
|
||||
(-> (conj v state) vec)))))
|
||||
|
||||
(on-change [_ ks]
|
||||
(doseq [k ks]
|
||||
(when-let [states (seq (@watchers k))]
|
||||
(apply stop states)
|
||||
(apply start states)))))
|
||||
|
||||
(defn restart-listener
|
||||
([]
|
||||
(restart-listener {}))
|
||||
([watchers]
|
||||
(RestartListener. (atom watchers))))
|
||||
|
||||
;; explicit, not composable (subject to depreciate?)
|
||||
|
||||
(defn stop-except [& states]
|
||||
(let [all (set (find-all-states))
|
||||
states (map var-to-str states)
|
||||
states (remove (set states) all)]
|
||||
(if-not (empty? states)
|
||||
(apply stop states)
|
||||
{:stopped #{}})))
|
||||
|
||||
(defn start-with-args [xs & states]
|
||||
(reset! -args xs)
|
||||
(if (first states)
|
||||
(apply start states)
|
||||
(start)))
|
||||
|
||||
(defn start-with [with]
|
||||
(doseq [[from to] with]
|
||||
(substitute! (var-to-str from)
|
||||
to :value))
|
||||
(start))
|
||||
|
||||
(defn start-with-states [with]
|
||||
(doseq [[from to] with]
|
||||
(substitute! (var-to-str from)
|
||||
to :state))
|
||||
(start))
|
||||
|
||||
(defn start-without [& states]
|
||||
(if (first states)
|
||||
(let [app (set (all-without-subs))
|
||||
states (map var-to-str states)
|
||||
without (remove (set states) app)]
|
||||
(if-not (empty? without)
|
||||
(apply start without)
|
||||
{:started #{}}))
|
||||
(start)))
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
(ns mount
|
||||
(:require [clojure.tools.macro :as macro]
|
||||
[clojure.tools.namespace.repl :refer [disable-reload!]]
|
||||
[clojure.tools.logging :refer [info debug error]]))
|
||||
|
||||
(disable-reload!)
|
||||
|
||||
;; (defonce ^:private session-id (System/currentTimeMillis))
|
||||
(defonce ^:private mount-state 42)
|
||||
(defonce ^:private state-seq (atom 0))
|
||||
(defonce ^:private state-order (atom {}))
|
||||
|
||||
(defn- make-state-seq [state]
|
||||
(or (@state-order state)
|
||||
(let [nseq (swap! state-seq inc)]
|
||||
(swap! state-order assoc state nseq)
|
||||
nseq)))
|
||||
|
||||
(deftype NotStartedState [state]
|
||||
Object
|
||||
(toString [this]
|
||||
(str "'" state "' is not started (to start all the states call mount/start)")))
|
||||
|
||||
;;TODO validate stop and the fact that start and stop are fns
|
||||
(defn- validate [{:keys [start stop]}]
|
||||
(when-not start
|
||||
(throw (IllegalArgumentException. "can't start a stateful thing without a start function. (i.e. missing :start fn)")))
|
||||
{:start start :stop stop})
|
||||
|
||||
(defmacro defstate [state & body]
|
||||
(let [[state [c cf d df]] (macro/name-with-attributes state body)
|
||||
{:keys [start stop]} (validate {c cf d df})]
|
||||
(let [s-meta (-> {:mount-state mount-state
|
||||
:order (make-state-seq state)
|
||||
:start `(fn [] (~@start))
|
||||
:started? false}
|
||||
(cond-> df (assoc :stop `(fn [] (~@stop)))))]
|
||||
`(defonce ~(with-meta state (merge (meta state) s-meta))
|
||||
(NotStartedState. ~(str state))))))
|
||||
|
||||
(defn- up [var {:keys [ns name start started?]}]
|
||||
(when-not started?
|
||||
(info ">> starting.. " name)
|
||||
(let [s (try (start)
|
||||
(catch Throwable t
|
||||
(throw (RuntimeException. (str "could not start [" name "] due to") t))))]
|
||||
(intern ns (symbol name) s)
|
||||
(alter-meta! var assoc :started? true))))
|
||||
|
||||
(defn- down [var {:keys [name stop started?]}]
|
||||
(when started?
|
||||
(info "<< stopping.. " name)
|
||||
(when stop
|
||||
(try
|
||||
(stop)
|
||||
(catch Throwable t
|
||||
(throw (RuntimeException. (str "could not stop [" name "] due to") t)))))
|
||||
(alter-meta! var assoc :started? false)))
|
||||
|
||||
(defn mount-state? [var]
|
||||
(= (-> var meta :mount-state)
|
||||
mount-state))
|
||||
|
||||
(defn find-all-states []
|
||||
(->> (all-ns)
|
||||
(mapcat ns-interns)
|
||||
(map second)
|
||||
(filter mount-state?)))
|
||||
|
||||
;; TODO: narrow down by {:mount {:include-ns
|
||||
;; {:starts-with ["app.foo" "bar.baz"]
|
||||
;; :nss ["app.nyse" "app.tools.datomic"] }
|
||||
;; :exclude-ns
|
||||
;; {:starts-with ["dont.want.this" "app.debug"]
|
||||
;; :nss ["dev" "app.stage"]}}}
|
||||
;;
|
||||
;; would come from boot/lein dev profile
|
||||
(defn- bring [states fun order]
|
||||
(->> states
|
||||
(sort-by (comp :order meta) order)
|
||||
(map #(fun % (meta %)))
|
||||
doall))
|
||||
|
||||
(defn start [& states]
|
||||
(let [states (or states (find-all-states))]
|
||||
(bring states up <)
|
||||
:started))
|
||||
|
||||
(defn stop [& states]
|
||||
(let [states (or states (find-all-states))]
|
||||
(bring states down >)
|
||||
:stopped))
|
||||
26
src/mount/tools/graph.cljc
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
(ns mount.tools.graph)
|
||||
|
||||
#?(:clj
|
||||
;;TODO ns based for now. need to be _state_ based. or better yet need to have a real graph :)
|
||||
(defn- add-deps [{:keys [ns] :as state} states]
|
||||
(let [refers (ns-refers ns)
|
||||
any (->> states vals (map :var) set)
|
||||
deps (->> (filter (comp any val) refers)
|
||||
(map (comp str second))
|
||||
set)]
|
||||
(assoc (dissoc state :ns)
|
||||
:deps deps))))
|
||||
|
||||
#?(:clj
|
||||
(defn- meta-with-ns [[sname {:keys [var] :as smeta}]]
|
||||
(let [sns (-> var meta :ns)]
|
||||
(assoc smeta :ns sns :name sname))))
|
||||
|
||||
#?(:clj
|
||||
(defn states-with-deps []
|
||||
(let [states @@#'mount.core/meta-state]
|
||||
(->> (map (comp #(add-deps % states)
|
||||
#(select-keys % [:name :order :ns :status])
|
||||
meta-with-ns)
|
||||
states)
|
||||
(sort-by :order)))))
|
||||
20
src/mount/tools/logger.cljc
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
(ns mount.tools.logger
|
||||
#?@(:cljs [(:require [goog.log :as glog])
|
||||
(:import [goog.debug Console])]))
|
||||
|
||||
#?(:cljs
|
||||
(defonce ^:dynamic *logger*
|
||||
(do
|
||||
(.setCapturing (Console.) true)
|
||||
(glog/getLogger "mount" nil))))
|
||||
|
||||
#?(:clj
|
||||
(defn log [msg & _]
|
||||
(prn msg)))
|
||||
|
||||
#?(:cljs
|
||||
(defn log [msg & level]
|
||||
(case (first level)
|
||||
:error (glog/error *logger* msg nil)
|
||||
(glog/info *logger* msg nil))))
|
||||
|
||||
53
src/mount/tools/macro.cljc
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
(ns mount.tools.macro
|
||||
(:refer-clojure :exclude [case])
|
||||
#?(:cljs (:require-macros [mount.tools.macrovich :refer [deftime]])
|
||||
:clj (:require [mount.tools.macrovich :refer [deftime]])))
|
||||
|
||||
(deftime
|
||||
|
||||
(defmacro on-error [msg f & {:keys [fail?]
|
||||
:or {fail? true}}]
|
||||
`(mount.tools.macrovich/case
|
||||
:clj (try ~f
|
||||
(catch Throwable t#
|
||||
(if ~fail?
|
||||
(throw (RuntimeException. ~msg t#))
|
||||
{:f-failed (ex-info ~msg {} t#)})))
|
||||
:cljs (try ~f
|
||||
(catch :default t#
|
||||
(if ~fail?
|
||||
(throw (js/Error (~'str ~msg " " t#)))
|
||||
{:f-failed (ex-info ~msg {} t#)})))))
|
||||
|
||||
(defmacro throw-runtime [msg]
|
||||
`(throw (mount.tools.macrovich/case :clj (RuntimeException. ~msg) :cljs (~'str ~msg))))
|
||||
|
||||
)
|
||||
|
||||
;; this is a one to one copy from https://github.com/clojure/tools.macro
|
||||
;; to avoid a lib dependency for a single function
|
||||
|
||||
(defn name-with-attributes
|
||||
"To be used in macro definitions.
|
||||
Handles optional docstrings and attribute maps for a name to be defined
|
||||
in a list of macro arguments. If the first macro argument is a string,
|
||||
it is added as a docstring to name and removed from the macro argument
|
||||
list. If afterwards the first macro argument is a map, its entries are
|
||||
added to the name's metadata map and the map is removed from the
|
||||
macro argument list. The return value is a vector containing the name
|
||||
with its extended metadata map and the list of unprocessed macro
|
||||
arguments."
|
||||
[name macro-args]
|
||||
(let [[docstring macro-args] (if (string? (first macro-args))
|
||||
[(first macro-args) (next macro-args)]
|
||||
[nil macro-args])
|
||||
[attr macro-args] (if (map? (first macro-args))
|
||||
[(first macro-args) (next macro-args)]
|
||||
[{} macro-args])
|
||||
attr (if docstring
|
||||
(assoc attr :doc docstring)
|
||||
attr)
|
||||
attr (if (meta name)
|
||||
(conj (meta name) attr)
|
||||
attr)]
|
||||
[(with-meta name attr) macro-args]))
|
||||
21
src/mount/tools/macrovich.cljc
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
(ns ^{:doc "From https://github.com/cgrand/macrovich. Licensed under EPL v1.
|
||||
Copyright Cristophe Grand." }
|
||||
mount.tools.macrovich
|
||||
(:refer-clojure :exclude [case]))
|
||||
|
||||
(defmacro deftime
|
||||
"This block will only be evaluated at the correct time for macro definition, at other times its content
|
||||
are removed.
|
||||
For Clojure it always behaves like a `do` block.
|
||||
For Clojurescript/JVM the block is only visible to Clojure.
|
||||
For self-hosted Clojurescript the block is only visible when defining macros in the pseudo-namespace."
|
||||
[& body]
|
||||
(when #?(:clj (not (:ns &env)) :cljs (re-matches #".*\$macros" (name (ns-name *ns*))))
|
||||
`(do ~@body)))
|
||||
|
||||
(defmacro case [& {:keys [cljs clj]}]
|
||||
(if (contains? &env '&env)
|
||||
`(if (:ns ~'&env) ~cljs ~clj)
|
||||
(if #?(:clj (:ns &env) :cljs true)
|
||||
cljs
|
||||
clj)))
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
(ns app.nyse
|
||||
(:require [mount :refer [defstate]]
|
||||
[datomic.api :as d]
|
||||
[clojure.tools.logging :refer [info]]
|
||||
[app.config :refer [app-config]]))
|
||||
|
||||
(defn- new-connection [conf]
|
||||
(info "conf: " conf)
|
||||
(let [uri (get-in conf [:datomic :uri])]
|
||||
(info "creating a connection to datomic:" uri)
|
||||
(d/create-database uri)
|
||||
(d/connect uri)))
|
||||
|
||||
(defn disconnect [conf conn]
|
||||
(let [uri (get-in conf [:datomic :uri])]
|
||||
(info "disconnecting from " uri)
|
||||
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
|
||||
(d/delete-database uri)))
|
||||
|
||||
(defstate conn :start (new-connection app-config)
|
||||
:stop (disconnect app-config conn))
|
||||
15
test/clj/tapp/conf.clj
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
(ns tapp.conf
|
||||
(:require [mount.core :as mount :refer [defstate]]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.tools.logging :refer [info]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn load-config [path]
|
||||
(info "loading config from" path)
|
||||
(-> path
|
||||
slurp
|
||||
edn/read-string))
|
||||
|
||||
(defstate config
|
||||
:start (load-config "dev/resources/config.edn"))
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
(ns app
|
||||
(ns tapp.example
|
||||
(:require [datomic.api :as d]
|
||||
[clojure.tools.nrepl.server :refer [start-server stop-server]]
|
||||
[mount :refer [defstate]]
|
||||
[app.utils.datomic :refer [touch]]
|
||||
[app.config :refer [app-config]]
|
||||
[app.nyse :as nyse]))
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.utils.datomic :refer [touch]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :as nyse]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
;; example on creating a network REPL
|
||||
(defn- start-nrepl [{:keys [host port]}]
|
||||
(start-server :bind host :port port))
|
||||
|
||||
;; nREPL is just another simple state
|
||||
(defstate nrepl :start (start-nrepl (:nrepl app-config))
|
||||
:stop (stop-server nrepl))
|
||||
(defstate nrepl :start (start-nrepl (:nrepl @config))
|
||||
:stop (stop-server @nrepl))
|
||||
|
||||
;; datomic schema
|
||||
(defn create-schema [conn]
|
||||
|
|
@ -44,7 +46,7 @@
|
|||
@(d/transact conn schema)))
|
||||
|
||||
(defn add-order [ticker bid offer qty] ;; can take connection as param
|
||||
@(d/transact nyse/conn [{:db/id (d/tempid :db.part/user)
|
||||
@(d/transact @nyse/conn [{:db/id (d/tempid :db.part/user)
|
||||
:order/symbol ticker
|
||||
:order/bid bid
|
||||
:order/offer offer
|
||||
|
|
@ -54,11 +56,11 @@
|
|||
(defn find-orders [ticker] ;; can take connection as param
|
||||
(let [orders (d/q '[:find ?e :in $ ?ticker
|
||||
:where [?e :order/symbol ?ticker]]
|
||||
(d/db nyse/conn) ticker)]
|
||||
(touch nyse/conn orders)))
|
||||
(d/db @nyse/conn) ticker)]
|
||||
(touch @nyse/conn orders)))
|
||||
|
||||
(defn create-nyse-schema []
|
||||
(create-schema nyse/conn))
|
||||
(create-schema @nyse/conn))
|
||||
|
||||
;; example of an app entry point
|
||||
(defn -main [& args]
|
||||
23
test/clj/tapp/nyse.clj
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
(ns tapp.nyse
|
||||
(:require [mount.core :as mount :refer [defstate]]
|
||||
[datomic.api :as d]
|
||||
[clojure.tools.logging :refer [info]]
|
||||
[tapp.conf :refer [config]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn- new-connection [conf]
|
||||
(info "conf: " conf)
|
||||
(let [uri (get-in @conf [:datomic :uri])]
|
||||
(info "creating a connection to datomic:" uri)
|
||||
(d/create-database uri)
|
||||
(d/connect uri)))
|
||||
|
||||
(defn disconnect [conf conn]
|
||||
(let [uri (get-in @conf [:datomic :uri])]
|
||||
(info "disconnecting from " uri)
|
||||
(.release @conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
|
||||
(d/delete-database uri)))
|
||||
|
||||
(defstate conn :start (new-connection config)
|
||||
:stop (disconnect config conn))
|
||||
14
test/clj/tapp/utils/datomic.clj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
(ns tapp.utils.datomic
|
||||
(:require [datomic.api :as d]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn entity [conn id]
|
||||
(d/entity (d/db conn) id))
|
||||
|
||||
(defn touch
|
||||
"takes 'entity ids' results from a query
|
||||
e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'"
|
||||
[conn results]
|
||||
(let [e (partial entity conn)]
|
||||
(map #(-> % first e d/touch) results)))
|
||||
43
test/clj/tapp/utils/logging.clj
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
(ns tapp.utils.logging ;; << change to your namespace/path
|
||||
(:require [mount.core]
|
||||
[robert.hooke :refer [add-hook clear-hooks]]
|
||||
[clojure.string :refer [split]]
|
||||
[clojure.tools.logging :refer [info]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn- f-to-action [f {:keys [status]}]
|
||||
(let [fname (-> (str f)
|
||||
(split #"@")
|
||||
first)]
|
||||
(case fname
|
||||
"mount.core$up" (when-not (:started status) :up)
|
||||
"mount.core$down" (when-not (:stopped status) :down)
|
||||
:noop)))
|
||||
|
||||
(defn whatcha-doing? [action]
|
||||
(case action
|
||||
:up ">> starting"
|
||||
:down "<< stopping"
|
||||
false))
|
||||
|
||||
(defn log-status [f & args]
|
||||
(let [[state-name state] args
|
||||
action (f-to-action f state)]
|
||||
(when-let [taking-over-the-world (whatcha-doing? action)]
|
||||
(info (str taking-over-the-world ".. " state-name)))
|
||||
(apply f args)))
|
||||
|
||||
(defonce lifecycle-fns
|
||||
#{#'mount.core/up
|
||||
#'mount.core/down})
|
||||
|
||||
(defn without-logging-status []
|
||||
(doall (map #(clear-hooks %) lifecycle-fns)))
|
||||
|
||||
|
||||
;; this is the one to use:
|
||||
|
||||
(defn with-logging-status []
|
||||
(without-logging-status)
|
||||
(doall (map #(add-hook % log-status) lifecycle-fns)))
|
||||
25
test/cljs/tapp/audit_log.cljs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
(ns tapp.audit-log
|
||||
(:require [datascript.core :as d]
|
||||
[cljs-time.core :refer [now]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defstate log :start (d/create-conn {}))
|
||||
|
||||
(defn audit [db source & msg]
|
||||
(d/transact! @db [{:db/id -1
|
||||
:source source
|
||||
:timestamp (now)
|
||||
:msg (apply str msg)}]))
|
||||
|
||||
(defn find-source-logs [db source]
|
||||
(d/q '{:find [?t ?msg]
|
||||
:in [$ ?s]
|
||||
:where [[?e :source ?s]
|
||||
[?e :timestamp ?t]
|
||||
[?e :msg ?msg]]}
|
||||
@@db source))
|
||||
|
||||
(defn find-all-logs [db]
|
||||
(->> (map :e (d/datoms @@db :aevt :timestamp))
|
||||
dedupe
|
||||
(d/pull-many @@db '[:timestamp :source :msg])))
|
||||
9
test/cljs/tapp/conf.cljs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
(ns tapp.conf
|
||||
(:require [tapp.audit-log :refer [audit log]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defn load-config [path]
|
||||
(audit log :app-conf "loading config from '" path "' (at least pretending)")
|
||||
{:system-a {:uri "ws://echo.websocket.org/"}})
|
||||
|
||||
(defstate config :start (load-config "resources/config.end"))
|
||||
26
test/cljs/tapp/example.cljs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
(ns tapp.example
|
||||
(:require [mount.core :as mount]
|
||||
[tapp.conf]
|
||||
[tapp.websockets]
|
||||
[tapp.audit-log :refer [log find-all-logs]]
|
||||
[cljs-time.format :refer [unparse formatters]]
|
||||
[hiccups.runtime :as hiccupsrt])
|
||||
(:require-macros [hiccups.core :as hiccups :refer [html]]))
|
||||
|
||||
(defn format-log-event [{:keys [timestamp source msg]}]
|
||||
(str (unparse (formatters :date-hour-minute-second-fraction) timestamp)
|
||||
" → [" (name source) "]: " msg))
|
||||
|
||||
(defn show-log []
|
||||
(.write js/document
|
||||
(html [:ul (doall (for [e (find-all-logs log)]
|
||||
[:li (format-log-event e)]))])))
|
||||
|
||||
(mount/start)
|
||||
|
||||
;; time to establish a websocket connection before disconnecting
|
||||
(js/setTimeout #(mount/stop-except "#'tapp.audit-log/log") 500)
|
||||
|
||||
;; time to close a connection to show it in audit
|
||||
(js/setTimeout #(show-log) 1000)
|
||||
|
||||
22
test/cljs/tapp/websockets.cljs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
(ns tapp.websockets
|
||||
(:require [tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [audit log]])
|
||||
(:require-macros [mount.core :refer [defstate]]))
|
||||
|
||||
(defn ws-status [ws]
|
||||
{:url (.-url ws) :ready-state (.-readyState ws)})
|
||||
|
||||
(defn connect [uri]
|
||||
(let [ws (js/WebSocket. uri)]
|
||||
(audit log :system-a "connecting to " (ws-status ws))
|
||||
(set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws)))
|
||||
(set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws)))
|
||||
ws))
|
||||
|
||||
(defn disconnect [ws]
|
||||
(audit log :system-a "closing " (ws-status @ws))
|
||||
(.close @ws)
|
||||
(audit log :system-a "disconnecting " (ws-status @ws)))
|
||||
|
||||
(defstate system-a :start (connect (get-in @config [:system-a :uri]))
|
||||
:stop (disconnect system-a))
|
||||
39
test/core/mount/test.cljc
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
(ns mount.test
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t]
|
||||
[doo.runner :refer-macros [doo-tests]]]
|
||||
:clj [[clojure.test :as t]])
|
||||
mount.core
|
||||
|
||||
mount.test.fun-with-values
|
||||
mount.test.private-fun
|
||||
mount.test.parts
|
||||
mount.test.cleanup-dirty-states
|
||||
mount.test.stop-except
|
||||
mount.test.start-without
|
||||
mount.test.start-with
|
||||
mount.test.start-with-states
|
||||
mount.test.printing
|
||||
))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(mount.core/in-cljc-mode)
|
||||
|
||||
#?(:cljs
|
||||
|
||||
;; (doo.runner/do-all-tests)
|
||||
(doo-tests
|
||||
'mount.test.fun-with-values
|
||||
'mount.test.private-fun
|
||||
'mount.test.parts
|
||||
'mount.test.cleanup-dirty-states
|
||||
'mount.test.stop-except
|
||||
'mount.test.start-without
|
||||
'mount.test.start-with
|
||||
'mount.test.start-with-states
|
||||
'mount.test.printing
|
||||
))
|
||||
|
||||
(defn run-tests []
|
||||
(t/run-all-tests #"mount.test.*"))
|
||||
40
test/core/mount/test/cleanup_deleted_states.cljc
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
(ns mount.test.cleanup-deleted-states
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.example]])
|
||||
[mount.test.helper :refer [dval helper forty-two]]))
|
||||
|
||||
(def status (atom :a-not-started))
|
||||
(defstate a :start (reset! status :a-started)
|
||||
:stop (reset! status :a-stopped))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
#?(:clj
|
||||
(deftest cleanup-deleted-state
|
||||
|
||||
(testing "should start all and remove/delete state from ns"
|
||||
(let [started (-> (mount/start) :started set)]
|
||||
(is (some #{"#'mount.test.cleanup-deleted-states/a"}
|
||||
started))
|
||||
(is (= :a-started @status))
|
||||
(ns-unmap 'mount.test.cleanup-deleted-states 'a)
|
||||
(is (nil? (resolve 'mount.test.cleanup-deleted-states/a)))))
|
||||
|
||||
(testing "should cleanup/stop a state after it was deleted from ns"
|
||||
(is (empty? (:started (mount/start)))) ;; on any mount op (not necessarily on "stop")
|
||||
(is (= :a-stopped @status))
|
||||
(is (not (some #{"#'mount.test.cleanup-deleted-states/a"}
|
||||
(keys @@#'mount.core/meta-state)))))
|
||||
|
||||
(testing "should not stop it again on stop (should not be there by this point)")
|
||||
(is (not (some #{"#'mount.test.cleanup-deleted-states/a"}
|
||||
(-> (mount/stop) :stopped set))))))
|
||||
|
||||
;; (t/run-tests)
|
||||
54
test/core/mount/test/cleanup_dirty_states.cljc
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
(ns mount.test.cleanup-dirty-states
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.example]])
|
||||
[mount.test.helper :refer [dval helper forty-two]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
#?(:clj
|
||||
(deftest cleanup-dirty-states
|
||||
(let [_ (mount/start)]
|
||||
(is (not (.isClosed (:server-socket (dval tapp.example/nrepl)))))
|
||||
(require 'tapp.example :reload)
|
||||
(mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
|
||||
(is (not (.isClosed (:server-socket (dval tapp.example/nrepl)))))
|
||||
(mount/stop)
|
||||
(is (instance? mount.core.NotStartedState (dval tapp.example/nrepl))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest restart-on-recompile
|
||||
(let [_ (mount/start)
|
||||
before (:server-socket (dval tapp.example/nrepl))]
|
||||
(require 'tapp.example :reload)
|
||||
(is (not= before (:server-socket (dval tapp.example/nrepl)))) ;; should have restarted on recompile/reload, hence different reference
|
||||
(mount/stop)
|
||||
(is (instance? mount.core.NotStartedState (dval tapp.example/nrepl))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest start-on-recompile
|
||||
(let [_ (mount/start)
|
||||
before (dval tapp.conf/config)]
|
||||
(require 'tapp.conf :reload)
|
||||
(is (not (identical? before (dval tapp.conf/config)))) ;; should be a newly recompiled map
|
||||
(mount/stop)
|
||||
(is (instance? mount.core.NotStartedState (dval tapp.conf/config))))))
|
||||
|
||||
#?(:cljs
|
||||
(deftest cleanup-dirty-states
|
||||
(let [_ (mount/start #'mount.test.helper/helper)]
|
||||
(is (= :started (dval helper)))
|
||||
(is (= 42 @forty-two))
|
||||
(.require js/goog "mount.test.helper") ;; should have run :stop of `helper`
|
||||
;; (is (= :cleaned @forty-two)) ;; TODO: figure out how to reload a namespace properly
|
||||
;; (is (instance? mount.core.NotStartedState (dval helper)))
|
||||
(mount/start #'mount.test.helper/helper)
|
||||
(is (= :started (dval helper)))
|
||||
(mount/stop)
|
||||
(is (instance? mount.core.NotStartedState (dval helper))))))
|
||||
190
test/core/mount/test/composable_fns.cljc
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
(ns mount.test.composable-fns
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[clojure.set :refer [intersection]]
|
||||
[mount.core :refer [only except swap swap-states with-args] :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[clojure.set :refer [intersection]]
|
||||
[clojure.tools.nrepl.server :refer [start-server stop-server]]
|
||||
[mount.core :as mount :refer [defstate only except swap swap-states with-args]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :refer [conn]]
|
||||
[tapp.example :refer [nrepl]]])
|
||||
[mount.test.helper :refer [dval helper]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defstate test-conn :start 42
|
||||
:stop (constantly 0))
|
||||
|
||||
(defstate test-nrepl :start [])
|
||||
|
||||
(def swap-conn {:start (fn [] 42)
|
||||
:stop #(println "stopping test-conn-state")})
|
||||
#?(:clj
|
||||
(def swap-nrepl {:start #(start-server :bind "localhost" :port 3442)
|
||||
:stop #(stop-server @nrepl)}))
|
||||
|
||||
#?(:clj
|
||||
(deftest only-states
|
||||
|
||||
(testing "only should only return given states.
|
||||
if source set of states is not provided, it should use all the states to select from"
|
||||
(is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl" "#'tapp.nyse/conn"}
|
||||
(only #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}))))
|
||||
|
||||
(testing "only should only return given states"
|
||||
(is (= #{"#'mount.test.composable-fns/test-conn" "#'tapp.example/nrepl"}
|
||||
(only [#'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn]
|
||||
#{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl}))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest except-states
|
||||
|
||||
(testing "except should exclude given states.
|
||||
if source set of states is not provided, it should use all the states to exclude from"
|
||||
(let [states (except #{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn})]
|
||||
(is (coll? states))
|
||||
(is (pos? (count states)))
|
||||
(is (zero? (count (intersection (set states)
|
||||
#{"#'tapp.example/nrepl" "#'tapp.nyse/conn" "#'is.not/here"}))))))
|
||||
|
||||
(testing "except should exclude given states"
|
||||
(is (= #{"#'tapp.conf/config" "#'mount.test.composable-fns/test-conn"}
|
||||
(set (except #{#'tapp.example/nrepl #'tapp.conf/config #'mount.test.composable-fns/test-conn}
|
||||
#{"#'is.not/here" #'tapp.example/nrepl #'tapp.nyse/conn})))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest states-with-args
|
||||
|
||||
(testing "with-args should set args and return all states if none provided"
|
||||
(let [states (with-args {:a 42})]
|
||||
(is (= {:a 42} (mount/args)))
|
||||
(is (= states (#'mount.core/find-all-states)))))
|
||||
|
||||
(testing "with-args should set args and thread states if provided"
|
||||
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}
|
||||
states (with-args t-states {:a 42})]
|
||||
(is (= {:a 42} (mount/args)))
|
||||
(is (= states t-states))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest swap-states-with-values
|
||||
|
||||
(testing "swap should swap states with values and return all states if none is given"
|
||||
(let [states (swap {#'tapp.nyse/conn "conn-sub"
|
||||
#'tapp.example/nrepl :nrepl-sub})]
|
||||
(is (= states (#'mount.core/find-all-states)))
|
||||
(mount/start)
|
||||
(is (map? (dval config)))
|
||||
(is (= :nrepl-sub (dval nrepl)))
|
||||
(is (= "conn-sub" (dval conn)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "swap should swap states with values and return only states that it is given"
|
||||
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.example/nrepl #'tapp.nyse/conn}
|
||||
states (swap t-states {#'tapp.nyse/conn "conn-sub"
|
||||
#'tapp.example/nrepl :nrepl-sub})]
|
||||
(is (= states t-states))
|
||||
(mount/start)
|
||||
(is (map? (dval config)))
|
||||
(is (= :nrepl-sub (dval nrepl)))
|
||||
(is (= "conn-sub" (dval conn)))
|
||||
(is (= 42 (dval test-conn)))
|
||||
(mount/stop)))))
|
||||
|
||||
#?(:clj
|
||||
(deftest swap-states-with-states
|
||||
|
||||
(testing "swap-states should swap states with states and return all mount states if none is given"
|
||||
(let [states (swap-states {#'tapp.nyse/conn swap-conn
|
||||
#'tapp.example/nrepl swap-nrepl})]
|
||||
(is (= states (#'mount.core/find-all-states)))
|
||||
(mount/start)
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (= 42 (dval conn)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "swap-states should swap states on start and rollback on stop"
|
||||
(let [states (swap-states {#'tapp.nyse/conn swap-conn})]
|
||||
(is (= states (#'mount.core/find-all-states)))
|
||||
(mount/start #'tapp.nyse/conn)
|
||||
(is (= 42 (dval conn)))
|
||||
(mount/stop #'tapp.nyse/conn)
|
||||
(mount/start #'tapp.nyse/conn)
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "swap-states should swap states with states and return only states that it is given"
|
||||
(let [t-states #{"#'is.not/here" #'mount.test.composable-fns/test-conn #'tapp.nyse/conn}
|
||||
states (swap-states t-states {#'tapp.nyse/conn swap-conn
|
||||
#'tapp.example/nrepl swap-nrepl})]
|
||||
(is (= states t-states))
|
||||
(apply mount/start states)
|
||||
(is (instance? mount.core.NotStartedState (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval nrepl)))
|
||||
(is (= 42 (dval conn)))
|
||||
(is (= 42 (dval test-conn))) ;; test-conn is explicitly started via "t-states"
|
||||
(mount/stop)))))
|
||||
|
||||
#?(:clj
|
||||
(deftest composing
|
||||
|
||||
(testing "states provided to the top level should narrow down the scope for the whole composition"
|
||||
(let [scope [#'tapp.conf/config
|
||||
#'tapp.example/nrepl
|
||||
#'tapp.nyse/conn
|
||||
#'mount.test.composable-fns/test-nrepl
|
||||
#'mount.test.composable-fns/test-conn]
|
||||
states (-> (only scope)
|
||||
(with-args {:a 42})
|
||||
(except [#'mount.test.composable-fns/test-nrepl
|
||||
#'mount.test.composable-fns/test-conn])
|
||||
(swap-states {#'tapp.example/nrepl swap-nrepl})
|
||||
(swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}}))]
|
||||
(is (= #{"#'tapp.nyse/conn" "#'tapp.conf/config" "#'tapp.example/nrepl"} (set states)))
|
||||
(mount/start states)
|
||||
(is (= {:a 42} (mount/args)))
|
||||
(is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should compose and start in a single composition"
|
||||
(let [scope [#'tapp.conf/config
|
||||
#'tapp.example/nrepl
|
||||
#'tapp.nyse/conn
|
||||
#'mount.test.composable-fns/test-nrepl
|
||||
#'mount.test.composable-fns/test-conn]]
|
||||
(-> (only scope)
|
||||
(with-args {:a 42})
|
||||
(except [#'mount.test.composable-fns/test-nrepl
|
||||
#'mount.test.composable-fns/test-conn])
|
||||
(swap-states {#'tapp.example/nrepl swap-nrepl})
|
||||
(swap {#'tapp.conf/config {:datomic {:uri "datomic:mem://composable-mount"}}})
|
||||
mount/start)
|
||||
(is (= {:a 42} (mount/args)))
|
||||
(is (= {:datomic {:uri "datomic:mem://composable-mount"}} (dval config)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should not start anything on empty seq of states"
|
||||
(let [scope #{}]
|
||||
(is (= {:started #{}} (-> (only scope)
|
||||
mount/start)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should not stop anything on empty seq of states"
|
||||
(let [scope #{}]
|
||||
(mount/start)
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (= {:stopped #{}} (-> (only scope)
|
||||
mount/stop)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(mount/stop)
|
||||
(is (instance? mount.core.NotStartedState (dval conn)))))))
|
||||
61
test/core/mount/test/fun_with_values.cljc
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
(ns mount.test.fun-with-values
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]])))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defn f [n]
|
||||
(fn [m]
|
||||
(+ n m)))
|
||||
|
||||
(defn g [a b]
|
||||
(+ a b))
|
||||
|
||||
(defn- pf [n]
|
||||
(+ 41 n))
|
||||
|
||||
(defn fna []
|
||||
42)
|
||||
|
||||
(defstate scalar :start 42)
|
||||
(defstate fun :start #(inc 41))
|
||||
(defstate with-fun :start (inc 41))
|
||||
(defstate with-partial :start (partial g 41))
|
||||
(defstate f-in-f :start (f 41))
|
||||
(defstate f-no-args-value :start (fna))
|
||||
(defstate f-no-args :start fna)
|
||||
(defstate f-args :start g)
|
||||
(defstate f-value :start (g 41 1))
|
||||
(defstate private-f :start pf)
|
||||
|
||||
(defn start-states []
|
||||
(mount/start #'mount.test.fun-with-values/scalar
|
||||
#'mount.test.fun-with-values/fun
|
||||
#'mount.test.fun-with-values/with-fun
|
||||
#'mount.test.fun-with-values/with-partial
|
||||
#'mount.test.fun-with-values/f-in-f
|
||||
#'mount.test.fun-with-values/f-args
|
||||
#'mount.test.fun-with-values/f-no-args-value
|
||||
#'mount.test.fun-with-values/f-no-args
|
||||
#'mount.test.fun-with-values/private-f
|
||||
#'mount.test.fun-with-values/f-value))
|
||||
|
||||
(use-fixtures :once
|
||||
#?(:cljs {:before start-states
|
||||
:after mount/stop}
|
||||
:clj #((start-states) (%) (mount/stop))))
|
||||
|
||||
(deftest fun-with-values
|
||||
(is (= @scalar 42))
|
||||
(is (= (@fun) 42))
|
||||
(is (= @with-fun 42))
|
||||
(is (= (@with-partial 1) 42))
|
||||
(is (= (@f-in-f 1) 42))
|
||||
(is (= @f-no-args-value 42))
|
||||
(is (= (@f-no-args) 42))
|
||||
(is (= (@f-args 41 1) 42))
|
||||
(is (= (@private-f 1) 42))
|
||||
(is (= @f-value 42)))
|
||||
28
test/core/mount/test/helper.cljc
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
(ns mount.test.helper
|
||||
(:require
|
||||
#?@(:cljs [[mount.core :as mount :refer-macros [defstate]]]
|
||||
:clj [[mount.core :as mount :refer [defstate]]])))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defn dval
|
||||
"returns a value of DerefableState without deref'ing it"
|
||||
[d]
|
||||
(-> (@@(var mount.core/meta-state)
|
||||
#?(:clj (.name d)
|
||||
:cljs (.-name d)))
|
||||
:inst
|
||||
deref))
|
||||
|
||||
(def forty-two (atom 42))
|
||||
|
||||
(defstate helper :start :started
|
||||
:stop (reset! forty-two :cleaned))
|
||||
|
||||
(def counter (atom {:a {:started 0 :stopped 0}
|
||||
:b {:started 0 :stopped 0}
|
||||
:c {:started 0 :stopped 0}}))
|
||||
|
||||
(defn inc-counter [state status]
|
||||
(swap! counter update-in [state status] inc)
|
||||
status)
|
||||
55
test/core/mount/test/on_reload.cljc
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
(ns mount.test.on-reload
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.example]])
|
||||
[mount.test.helper :refer [dval helper forty-two counter inc-counter]]
|
||||
[mount.test.on-reload-helper :refer [a b c]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
#?(:clj
|
||||
(defn abc [f]
|
||||
(mount/start #'mount.test.on-reload-helper/a
|
||||
#'mount.test.on-reload-helper/b
|
||||
#'mount.test.on-reload-helper/c)
|
||||
(f)
|
||||
(mount/stop)))
|
||||
|
||||
(use-fixtures :each
|
||||
#?(:cljs {:before #(mount/start #'mount.test.on-reload-helper/a
|
||||
#'mount.test.on-reload-helper/b
|
||||
#'mount.test.on-reload-helper/c)
|
||||
:after mount/stop}
|
||||
:clj abc))
|
||||
|
||||
#?(:clj
|
||||
(deftest restart-by-default
|
||||
(is (= '(:started) (distinct (map dval [a b c]))))
|
||||
(let [pre-reload @counter]
|
||||
(require 'mount.test.on-reload-helper :reload)
|
||||
|
||||
;; "a" is marked as :noop on reload
|
||||
;; previous behavior left a stale reference =>>> ;; (is (instance? mount.core.NotStartedState (dval a))) ;; (!) stale reference of old a is still there somewhere
|
||||
(is (= :started (dval a))) ;; make sure a still has the same instance as before reload
|
||||
(is (= (-> pre-reload :a) ;; and the start was not called: the counter did not change
|
||||
(-> @counter :a)))
|
||||
|
||||
;; "b" is marked as :stop on reload
|
||||
(is (instance? mount.core.NotStartedState (dval b)))
|
||||
(is (= (-> pre-reload :b :started)
|
||||
(-> @counter :b :started)))
|
||||
(is (= (inc (-> pre-reload :b :stopped))
|
||||
(-> @counter :b :stopped)))
|
||||
|
||||
;; "c" is not marked on reload, using "restart" as default
|
||||
(is (= :started (dval c)))
|
||||
(is (= (inc (-> pre-reload :c :started))
|
||||
(-> @counter :c :started)))
|
||||
(is (= (inc (-> pre-reload :c :stopped))
|
||||
(-> @counter :c :stopped))))))
|
||||
21
test/core/mount/test/on_reload_helper.cljc
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
(ns mount.test.on-reload-helper
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.example]])
|
||||
[mount.test.helper :refer [inc-counter]]))
|
||||
|
||||
(defstate ^{:on-reload :noop} a :start (inc-counter :a :started)
|
||||
:stop (inc-counter :a :stopped))
|
||||
|
||||
(defstate ^{:on-reload :stop} b :start (inc-counter :b :started)
|
||||
:stop (inc-counter :b :stopped))
|
||||
|
||||
(defstate c :start (inc-counter :c :started)
|
||||
:stop (inc-counter :c :stopped))
|
||||
|
||||
38
test/core/mount/test/parts.cljc
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
(ns mount.test.parts
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.nyse :refer [conn]]])
|
||||
[mount.test.helper :refer [dval]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defstate should-not-start :start (constantly 42))
|
||||
|
||||
#?(:clj
|
||||
(defn with-parts [f]
|
||||
(mount/start #'tapp.conf/config #'tapp.nyse/conn)
|
||||
(f)
|
||||
(mount/stop)))
|
||||
|
||||
(use-fixtures :once
|
||||
#?(:cljs {:before #(mount/start #'tapp.conf/config #'tapp.audit-log/log)
|
||||
:after mount/stop}
|
||||
:clj with-parts))
|
||||
|
||||
#?(:clj
|
||||
(deftest start-only-parts
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval should-not-start)))))
|
||||
|
||||
#?(:cljs
|
||||
(deftest start-only-parts
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (map? (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval should-not-start)))
|
||||
(is (instance? mount.core.NotStartedState (dval system-a)))))
|
||||
17
test/core/mount/test/printing.cljc
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
(ns mount.test.printing
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]])))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defstate foo
|
||||
:start (do (println "Starting!") 42))
|
||||
|
||||
(deftest test-printing-has-no-side-effects
|
||||
;; Test that printing an unstarted DerefableState does not have the
|
||||
;; side-effect of starting it
|
||||
(println foo)
|
||||
(is (not= 42 foo)))
|
||||
20
test/core/mount/test/private_fun.cljc
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
(ns mount.test.private-fun
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]])
|
||||
|
||||
[mount.test.fun-with-values :refer [private-f]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(use-fixtures :once
|
||||
#?(:cljs {:before #(mount/start #'mount.test.fun-with-values/private-f)
|
||||
:after mount/stop}
|
||||
:clj #((mount/start #'mount.test.fun-with-values/private-f)
|
||||
(%)
|
||||
(mount/stop))))
|
||||
|
||||
(deftest fun-with-values
|
||||
(is (= (@private-f 1) 42)))
|
||||
83
test/core/mount/test/start_with.cljc
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
(ns mount.test.start-with
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :refer [conn]]
|
||||
[tapp.example :refer [nrepl]]])
|
||||
[mount.test.helper :refer [dval helper]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defstate test-conn :start 42
|
||||
:stop (constantly 0))
|
||||
|
||||
(defstate test-nrepl :start [])
|
||||
|
||||
#?(:cljs
|
||||
(deftest start-with
|
||||
|
||||
(testing "should start with substitutes"
|
||||
(let [_ (mount/start-with {#'tapp.websockets/system-a "system-a-sub"
|
||||
#'mount.test.helper/helper "helper-sub"})]
|
||||
(is (map? (dval config)))
|
||||
(is (= "helper-sub" (dval helper)))
|
||||
(is (= "system-a-sub" (dval system-a)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after start-with"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? js/WebSocket (dval system-a)))
|
||||
(is (= 42 (dval test-conn)))
|
||||
(is (vector? (dval test-nrepl)))
|
||||
(is (= :started (dval helper)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start-without normally after start-with"
|
||||
(let [_ (mount/start-without #'mount.test.start-with/test-conn
|
||||
#'mount.test.start-with/test-nrepl)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? js/WebSocket (dval system-a)))
|
||||
(is (= :started (dval helper)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
|
||||
(mount/stop)))))
|
||||
|
||||
#?(:clj
|
||||
(deftest start-with
|
||||
|
||||
(testing "should start with substitutes"
|
||||
(let [_ (mount/start-with {#'tapp.nyse/conn "conn-sub"
|
||||
#'tapp.example/nrepl :nrepl-sub})]
|
||||
(is (map? (dval config)))
|
||||
(is (= :nrepl-sub (dval nrepl)))
|
||||
(is (= "conn-sub" (dval conn)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after start-with"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (= (dval test-conn) 42))
|
||||
(is (vector? (dval test-nrepl)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start-without normally after start-with"
|
||||
(let [_ (mount/start-without #'mount.test.start-with/test-conn
|
||||
#'mount.test.start-with/test-nrepl)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
|
||||
(mount/stop)))))
|
||||
105
test/core/mount/test/start_with_states.cljc
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
(ns mount.test.start-with-states
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[clojure.tools.nrepl.server :refer [start-server stop-server]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :refer [conn]]
|
||||
[tapp.example :refer [nrepl]]])
|
||||
[mount.test.helper :refer [dval helper]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
(defstate test-conn :start 42
|
||||
:stop (constantly 0))
|
||||
|
||||
(defstate test-nrepl :start [])
|
||||
|
||||
(def swap-conn {:start (fn [] 42)
|
||||
:stop #(println "stopping test-conn-state")})
|
||||
#?(:clj
|
||||
(def swap-nrepl {:start #(start-server :bind "localhost" :port 3442)
|
||||
:stop #(stop-server @nrepl)})
|
||||
:cljs
|
||||
(def swap-nrepl {:start (fn [] :nrepl)
|
||||
:stop (fn [] :stopped-nrepl)}))
|
||||
|
||||
#?(:cljs
|
||||
(deftest start-with-states
|
||||
|
||||
(testing "should start with substitutes"
|
||||
(let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn
|
||||
#'mount.test.helper/helper swap-nrepl})]
|
||||
(is (map? (dval config)))
|
||||
(is (= (:nrepl (dval helper))))
|
||||
(is (= (dval system-a) 42))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(mount/stop)))
|
||||
|
||||
#_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states
|
||||
(let [_ (mount/start-with-states {#'tapp.websockets/system-a swap-conn})]
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (= 42 (dval system-a)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after start-with-states"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? js/WebSocket (dval system-a)))
|
||||
(is (= 42 (dval test-conn)))
|
||||
(is (vector? (dval test-nrepl)))
|
||||
(is (= :started (dval helper)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start-without normally after start-with-states"
|
||||
(let [_ (mount/start-without #'mount.test.start-with-states/test-conn
|
||||
#'mount.test.start-with-states/test-nrepl)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? js/WebSocket (dval system-a)))
|
||||
(is (= :started (dval helper)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
|
||||
(mount/stop)))))
|
||||
|
||||
#?(:clj
|
||||
(deftest start-with-states
|
||||
|
||||
(testing "should start with substitutes"
|
||||
(let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn
|
||||
#'tapp.example/nrepl swap-nrepl})]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (= (dval conn) 42))
|
||||
(mount/stop)))
|
||||
|
||||
#_(testing "should not start the substitute itself" ;; was true when subbing with exsiting states
|
||||
(let [_ (mount/start-with-states {#'tapp.nyse/conn swap-conn})]
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (= (dval conn) 42))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after start-with-states"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (= (dval test-conn) 42))
|
||||
(is (vector? (dval test-nrepl)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start-without normally after start-with-states"
|
||||
(let [_ (mount/start-without #'mount.test.start-with-states/test-conn
|
||||
#'mount.test.start-with-states/test-nrepl)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
|
||||
(mount/stop)))))
|
||||
39
test/core/mount/test/start_without.cljc
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
(ns mount.test.start-without
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :refer [conn]]
|
||||
[tapp.example :refer [nrepl]]])
|
||||
[mount.test.helper :refer [dval helper]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
#?(:clj
|
||||
(defn without [f]
|
||||
(mount/start-without #'tapp.nyse/conn #'tapp.example/nrepl)
|
||||
(f)
|
||||
(mount/stop)))
|
||||
|
||||
(use-fixtures :once
|
||||
#?(:cljs {:before #(mount/start-without #'mount.test.helper/helper #'tapp.websockets/system-a)
|
||||
:after mount/stop}
|
||||
:clj without))
|
||||
|
||||
#?(:clj
|
||||
(deftest start-without-states
|
||||
(is (map? (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval nrepl)))
|
||||
(is (instance? mount.core.NotStartedState (dval conn)))))
|
||||
|
||||
#?(:cljs
|
||||
(deftest start-without-states
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? mount.core.NotStartedState (dval helper)))
|
||||
(is (instance? mount.core.NotStartedState (dval system-a)))))
|
||||
68
test/core/mount/test/stop_except.cljc
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
(ns mount.test.stop-except
|
||||
(:require
|
||||
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer-macros [defstate]]
|
||||
[tapp.websockets :refer [system-a]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.audit-log :refer [log]]]
|
||||
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[tapp.conf :refer [config]]
|
||||
[tapp.nyse :refer [conn]]
|
||||
[tapp.example :refer [nrepl]]])
|
||||
[mount.test.helper :refer [dval helper]]))
|
||||
|
||||
#?(:clj (alter-meta! *ns* assoc ::load false))
|
||||
|
||||
#?(:cljs
|
||||
(deftest stop-except
|
||||
|
||||
(testing "should stop all except nrepl"
|
||||
(let [_ (mount/start)
|
||||
_ (mount/stop-except #'tapp.audit-log/log #'mount.test.helper/helper)]
|
||||
(is (= :started (dval helper)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(is (instance? mount.core.NotStartedState (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval system-a)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after stop-except"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? js/WebSocket (dval system-a)))
|
||||
(is (instance? datascript.db/DB @(dval log)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should stop all normally after stop-except"
|
||||
(let [_ (mount/start)
|
||||
_ (mount/stop-except #'tapp.audit-log/log #'mount.test.helper/helper)
|
||||
_ (mount/stop)]
|
||||
(is (instance? mount.core.NotStartedState (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval log)))
|
||||
(is (instance? mount.core.NotStartedState (dval system-a)))))))
|
||||
|
||||
#?(:clj
|
||||
(deftest stop-except
|
||||
|
||||
(testing "should stop all except nrepl"
|
||||
(let [_ (mount/start)
|
||||
_ (mount/stop-except #'tapp.nyse/conn #'tapp.conf/config)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval nrepl)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should start normally after stop-except"
|
||||
(let [_ (mount/start)]
|
||||
(is (map? (dval config)))
|
||||
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
|
||||
(is (instance? datomic.peer.LocalConnection (dval conn)))
|
||||
(mount/stop)))
|
||||
|
||||
(testing "should stop all normally after stop-except"
|
||||
(let [_ (mount/start)
|
||||
_ (mount/stop-except #'tapp.nyse/conn #'tapp.conf/config)
|
||||
_ (mount/stop)]
|
||||
(is (instance? mount.core.NotStartedState (dval config)))
|
||||
(is (instance? mount.core.NotStartedState (dval conn)))
|
||||
(is (instance? mount.core.NotStartedState (dval nrepl)))))))
|
||||
61
test/core/mount/test/var/fun_with_values.clj
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
(ns mount.test.var.fun-with-values
|
||||
(:require [clojure.test :as t :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn f [n]
|
||||
(fn [m]
|
||||
(+ n m)))
|
||||
|
||||
(defn g [a b]
|
||||
(+ a b))
|
||||
|
||||
(defn- pf [n]
|
||||
(+ 41 n))
|
||||
|
||||
(defn fna []
|
||||
42)
|
||||
|
||||
(defstate scalar :start 42)
|
||||
(defstate fun :start #(inc 41))
|
||||
(defstate with-fun :start (inc 41))
|
||||
(defstate with-partial :start (partial g 41))
|
||||
(defstate f-in-f :start (f 41))
|
||||
(defstate f-no-args-value :start (fna))
|
||||
(defstate f-no-args :start fna)
|
||||
(defstate f-args :start g)
|
||||
(defstate f-value :start (g 41 1))
|
||||
(defstate private-f :start pf)
|
||||
|
||||
(defn start-states []
|
||||
(mount/in-clj-mode)
|
||||
(require :reload 'mount.test.var.fun-with-values)
|
||||
(mount/start #'mount.test.var.fun-with-values/scalar
|
||||
#'mount.test.var.fun-with-values/fun
|
||||
#'mount.test.var.fun-with-values/with-fun
|
||||
#'mount.test.var.fun-with-values/with-partial
|
||||
#'mount.test.var.fun-with-values/f-in-f
|
||||
#'mount.test.var.fun-with-values/f-args
|
||||
#'mount.test.var.fun-with-values/f-no-args-value
|
||||
#'mount.test.var.fun-with-values/f-no-args
|
||||
#'mount.test.var.fun-with-values/private-f
|
||||
#'mount.test.var.fun-with-values/f-value))
|
||||
|
||||
(defn stop-states []
|
||||
(mount/stop)
|
||||
(mount/in-cljc-mode))
|
||||
|
||||
(use-fixtures :once #((start-states) (%) (stop-states)))
|
||||
|
||||
(deftest fun-with-values
|
||||
(is (= scalar 42))
|
||||
(is (= (fun) 42))
|
||||
(is (= with-fun 42))
|
||||
(is (= (with-partial 1) 42))
|
||||
(is (= (f-in-f 1) 42))
|
||||
(is (= f-no-args-value 42))
|
||||
(is (= (f-no-args) 42))
|
||||
(is (= (f-args 41 1) 42))
|
||||
(is (= (private-f 1) 42))
|
||||
(is (= f-value 42)))
|
||||
19
test/core/mount/test/var/private_fun.clj
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
(ns mount.test.var.private-fun
|
||||
(:require [clojure.test :refer [is are deftest testing use-fixtures]]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[mount.test.var.fun-with-values :refer [private-f]]))
|
||||
|
||||
(alter-meta! *ns* assoc ::load false)
|
||||
|
||||
(defn in-clj-mode [f]
|
||||
(mount/in-clj-mode)
|
||||
(require :reload 'mount.test.var.fun-with-values 'mount.test.var.private-fun)
|
||||
(mount/start #'mount.test.var.fun-with-values/private-f)
|
||||
(f)
|
||||
(mount/stop)
|
||||
(mount/in-cljc-mode))
|
||||
|
||||
(use-fixtures :once in-clj-mode)
|
||||
|
||||
(deftest fun-with-values
|
||||
(is (= (private-f 1) 42)))
|
||||
29
test/core/mount/test_self_host.cljs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
(ns mount.test-self-host
|
||||
(:require
|
||||
[cljs.test :as t]
|
||||
|
||||
mount.test.fun-with-values
|
||||
mount.test.private-fun
|
||||
mount.test.printing
|
||||
mount.test.parts
|
||||
mount.test.cleanup-dirty-states
|
||||
mount.test.stop-except
|
||||
mount.test.start-without
|
||||
mount.test.start-with
|
||||
mount.test.start-with-states
|
||||
))
|
||||
|
||||
(t/run-tests
|
||||
'mount.test.fun-with-values
|
||||
'mount.test.private-fun
|
||||
'mount.test.printing
|
||||
'mount.test.parts
|
||||
'mount.test.cleanup-dirty-states
|
||||
;; 'mount.test.stop-except ;; TODO: can't run with deps.edn (due to "WebSocket is not defined")
|
||||
;; 'mount.test.start-with ;; boot, lein have no problems
|
||||
;; 'mount.test.start-with-states ;; most likely somm misconfigured in node..
|
||||
'mount.test.start-without
|
||||
)
|
||||
|
||||
(defn run-tests []
|
||||
(t/run-all-tests #"mount.test.*"))
|
||||
1
test/resources/mount.cljs.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
{:require [mount.test]}
|
||||