[uberjar]: lifecycle taking fns and values

This commit is contained in:
anatoly 2015-11-27 12:21:06 -05:00
parent 897f997630
commit 18e4e229ce
14 changed files with 22 additions and 519 deletions

View file

@ -72,16 +72,16 @@ mount is an alternative to the [component](https://github.com/stuartsierra/compo
Creating state is easy: Creating state is easy:
```clojure ```clojure
(defstate conn :start (create-conn)) (defstate conn :start create-conn)
``` ```
where `(create-conn)` is defined elsewhere, can be right above it. where `create-conn` function 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 / destryed between reloads, there is also `:stop`
```clojure ```clojure
(defstate conn :start (create-conn) (defstate conn :start create-conn
:stop (disconnect conn)) :stop #(disconnect conn))
``` ```
That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply
@ -129,7 +129,7 @@ There are of course direct dependecies that `mount` respects:
(:require [mount.core :refer [defstate]])) (:require [mount.core :refer [defstate]]))
(defstate app-config (defstate app-config
:start (load-config "test/resources/config.edn")) :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 `app-config`, being top level, can be used in other namespaces, including the ones that create states:
@ -139,7 +139,7 @@ this `app-config`, being top level, can be used in other namespaces, including t
(:require [mount.core :refer [defstate]] (:require [mount.core :refer [defstate]]
[app.config :refer [app-config]])) [app.config :refer [app-config]]))
(defstate conn :start (create-connection app-config)) (defstate conn :start #(create-connection app-config))
``` ```
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj) [here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)

View file

@ -1,8 +1,6 @@
(ns dev (ns dev
"Tools for interactive development with the REPL. This file should "Tools for interactive development with the REPL. This file should
not be included in a production build of the application." 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] (:require [clojure.java.io :as io]
[clojure.java.javadoc :refer [javadoc]] [clojure.java.javadoc :refer [javadoc]]
[clojure.pprint :refer [pprint]] [clojure.pprint :refer [pprint]]
@ -11,7 +9,7 @@
[clojure.set :as set] [clojure.set :as set]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :as test] [clojure.test :as test]
;; [clojure.core.async :refer [>!! <!! >! <! go-loop alt! timeout]] [app.utils.logging :refer [with-logging-status]]
[clojure.tools.namespace.repl :as tn] [clojure.tools.namespace.repl :as tn]
[mount.core :as mount] [mount.core :as mount]
[app.www] [app.www]
@ -43,3 +41,5 @@
[] []
(stop) (stop)
(tn/refresh :after 'dev/go)) (tn/refresh :after 'dev/go))
(with-logging-status) ;; to demo REPL time logging

View file

@ -1,197 +0,0 @@
###### Differences from "Component"
## Perception
Solving the "application state" in Clojure, where an application is not a tool or a library,
but a product that has lots of state to deal with, is not a trivial task.
The [Component](https://github.com/stuartsierra/component) framework is a solution that has been gaining popularity:
> _[source](http://www.javacodegeeks.com/2015/09/clojure-web-development-state-of-the-art.html):_
> _I think all agreed that Component is the industry standard for managing lifecycle of Clojure applications. If you are a Java developer you may think of it as a Spring (DI) replacement you declare dependencies between “components” which are resolved on “system” startup. So you just say “my component needs a repository/database pool” and component library “injects” it for you._
While this is a common understanding, the Component is far from being Spring, in a good sense:
* its codebase is fairly small
* 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)
- [Objects vs. Namespaces](#objects-vs-namespaces)
- [Start and Stop Order](#start-and-stop-order)
- [Component requires whole app buy in](#component-requires-whole-app-buy-in)
- [Refactoring an existing application](#refactoring-an-existing-application)
- [Code navigation](#code-navigation)
- [Starting and stopping _parts_ of an application](#starting-and-stopping-_parts_-of-an-application)
- [Boilerplate code](#boilerplate-code)
- [What Component does better](#what-component-does-better)
- [Swapping alternate implementations](#swapping-alternate-implementations)
- [Uberjar / Packaging](#uberjar--packaging)
- [Multiple separate systems](#multiple-separate-systems)
- [Visualizing dependency graph](#visualizing-dependency-graph)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Then why "mount"!?
[mount](https://github.com/tolitius/mount) was created after using Component for several projects.
While Component is an interesting way to manage state, it has its limitations that prevented us
from having the ultimate super power of Clojure: _fun working with it_. Plus several other disadvantages
that we wanted to "fix".
## 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
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
of Namespaces and Functions to a "_closed_" application of Objects and Methods. "open" and "close"
here are rather feelings, but it is way easier and more natural to
* go to a namespace to see this function
than to
* go to a namespace, go to a component, go to another component that this function maybe using/referenced at via a component key, to get the full view of the function.
Again this is mostly a personal preference: the code works in both cases.
### Refactoring an existing application
Since to get the most benefits of Component the approach is "all or nothing", to rewrite an existing application
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
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 (`:require`/`:use`), navigation across functions / states is exactly
the same with or without Mount: there are no extra mental steps.
### Starting and stopping _parts_ of an application
Component can't really start and stop parts of an application within the same "system". Other sub systems can be
created from scratch or by dissoc'ing / merging with existing systems, but it is usually not all
that flexible in terms of REPL sessions where lots of time is spent.
Mount _can_ start and stop parts of an application via given states with their namespaces:
```clojure
dev=> (mount/start #'app.config/app-config #'app.nyse/conn)
11:35:06.753 [nREPL-worker-1] INFO mount - >> starting.. app-config
11:35:06.756 [nREPL-worker-1] INFO mount - >> starting.. conn
:started
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:
* a system with dependencies
* components as records
* with optional constructors
* and a Lifecycle/start Lifecycle/stop implementations
* destructuring component maps
Depending on the number of application components the "extra" size may vary.
Mount is pretty much:
```clojure
(defstate name :start (fn)
:stop (fn))
```
no "ceremony".
## What Component does better
### Swapping alternate implementations
This is someting that is very useful for testing and is very easy to do in Component by simply assoc'ing onto a map.
Mount can do it to: https://github.com/tolitius/mount#swapping-alternate-implementations
The reason it is in "Component does it better" section is because, while result is the same, merging maps is a bit simpler than:
```clojure
(mount/start-with {#'app.nyse/db #'app.test/test-db
#'app.nyse/publisher #'app.test/test-publisher})
```
### Uberjar / Packaging
Since Component fully controls the `system` where the whole application lives, it is quite simple
to start an application from anywhere including a `-main` function of the uberjar.
In order to start the whole system in development, Mount just needs `(mount/start)` or `(reset)`
it's [simple](https://github.com/tolitius/mount#the-importance-of-being-reloadable).
However there is no "tools.namespaces"/REPL at a "stand alone jar runtime" and in order for Mount to start / stop
the app, states need to be `:require`/`:use`d, which is usually done within the same namespace as `-main`.
Depending on app dependencies, it could only require a few states to be `:require`/`:use`d, others
will be brought transitively. Here is an [example](uberjar.md#creating-reloadable-uberjarable-app) of building a wepapp uberjar with Mount.
On the flip side, Component _system_ usually requires lots of `:require`s as well, since in order to be built, it needs to "see" all the top level states.
###### _conclusion: it's simple in Mount as well, but requires an additional step._
### Multiple separate systems
With Component multiple separate systems can be started _in the same Clojure runtime_ with different settings. Which is very useful for testing.
Mount keeps states in namespaces, hence the app becomes "[The One](https://en.wikipedia.org/wiki/Neo_(The_Matrix))", and there can't be "multiples The Ones".
Testing is not alien to Mount and it knows how to do a thing or two:
* [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)
* and [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations)
But running two apps in the same JVM side by side with "same but different" states, is not something Mount can do at the moment.
###### _conclusion: needs more thinking._
### Visualizing dependency graph
Component keeps an actual graph which can be visualized with great libraries like [loom](https://github.com/aysylu/loom).
Having this visualization is really helpful, especially during code discusions between multiple developers.
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,3 +0,0 @@
# Introduction to statuo
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

View file

@ -1,130 +0,0 @@
## 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.2.0-SNAPSHOT-standalone.jar
```
Since we have a default for a Datomic URI, it'll work with no arguments:
```bash
$ java -jar target/mount-0.2.0-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.2.0-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.2.0-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]`.

View file

@ -1,163 +0,0 @@
## Creating Reloadable Uberjar'able App
This example lives in the `uberjar` branch. If you'd like to follow along:
```bash
$ git checkout uberjar
Switched to branch 'uberjar'
```
### App state
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
```
where `nyse-app` is _the_ app. It has the usual routes:
```clojure
(defroutes mount-example-routes
(GET "/" [] "welcome to mount sample app!")
(GET "/nyse/orders/:ticker" [ticker]
(generate-string (find-orders ticker)))
(POST "/nyse/orders" [ticker qty bid offer]
(add-order ticker (bigdec bid) (bigdec offer) (Integer/parseInt qty))
(generate-string {:added {:ticker ticker
:qty qty
:bid bid
:offer offer}})))
```
and the reloadable state:
```clojure
(defn start-nyse []
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
(-> (routes mount-example-routes)
(handler/site)
(run-jetty {:join? false
:port (get-in app-config [:www :port])})))
(defstate nyse-app :start (start-nyse)
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
```
In order not to block, and being reloadable, the Jetty server is started in the "`:join? false`" mode which starts the server,
and just returns a reference to it, so it can be easily stopped by `(.stop server)`
### "Uberjar is the :main"
In order for a standalone jar to run, it needs an entry point. This sample app [has one](https://github.com/tolitius/mount/blob/uberjar/test/app/app.clj#L16):
```clojure
;; example of an app entry point
(defn -main [& args]
(mount/start))
```
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}}
```
### REPL time
```clojure
$ 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
dev=>
```
Jetty server is started and ready to roll. And everything is still reloadable:
```clojure
dev=> (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
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
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
```
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):
```clojure
:www {:port 0} ;; Jetty will randomly assign the available port (this is good for dev reloadability)
```
This of course can be solidified for different env deployments. For example I like `4242` :)
### 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
```
Let's give it a spin:
```bash
$ java -jar target/mount-0.1.0-SNAPSHOT-standalone.jar
...
16:51:35.586 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:54728
```
Up and running on port `:54728`:
<img src="img/welcome-uberjar.png" width="250">
See if we have any orders:
<img src="img/get-uberjar.png" width="400">
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"
{"added":{"ticker":"GOOG","qty":"100","bid":"665.51","offer":"665.59"}}
```
now we should:
<img src="img/post-uberjar.png" width="400">
### Choices
There are multiple ways to start a web app. This above is the most straighforward one: start server / stop server.
But depending on the requirements / architecture, the app can also have an entry point to `(mount/start)`
via something like [:ring :init](https://github.com/weavejester/lein-ring#general-options)). Or the `(mount/start)`
can go into the handler function, etc.

View file

@ -35,16 +35,16 @@
(validate lifecycle) (validate lifecycle)
(let [s-meta (cond-> {:mount-state mount-state (let [s-meta (cond-> {:mount-state mount-state
:order (make-state-seq state) :order (make-state-seq state)
:start `(fn [] (~@start)) :start `(fn [] ~start)
:started? false} :started? false}
stop (assoc :stop `(fn [] (~@stop))) stop (assoc :stop `(fn [] (~stop)))
suspend (assoc :suspend `(fn [] (~@suspend))) suspend (assoc :suspend `(fn [] (~suspend)))
resume (assoc :resume `(fn [] (~@resume))))] resume (assoc :resume `(fn [] (~resume))))]
`(defonce ~(with-meta state (merge (meta state) s-meta)) `(defonce ~(with-meta state (merge (meta state) s-meta))
(NotStartedState. ~(str state)))))) (NotStartedState. ~(str state))))))
(defn- record! [{:keys [ns name]} f done] (defn- record! [{:keys [ns name]} f done]
(let [state (f)] (let [state (trampoline f)]
(swap! done conj (ns-resolve ns name)) (swap! done conj (ns-resolve ns name))
state)) state))

View file

@ -10,8 +10,8 @@
(start-server :bind host :port port)) (start-server :bind host :port port))
;; nREPL is just another simple state ;; nREPL is just another simple state
(defstate nrepl :start (start-nrepl (:nrepl app-config)) (defstate nrepl :start #(start-nrepl (:nrepl app-config))
:stop (stop-server nrepl)) :stop #(stop-server nrepl))
;; example of an app entry point ;; example of an app entry point
(defn -main [& args] (defn -main [& args]

View file

@ -10,4 +10,4 @@
edn/read-string)) edn/read-string))
(defstate app-config (defstate app-config
:start (load-config "test/resources/config.edn")) :start #(load-config "test/resources/config.edn"))

View file

@ -17,8 +17,8 @@
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) (.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
(d/delete-database uri))) (d/delete-database uri)))
(defstate conn :start (new-connection app-config) (defstate conn :start #(new-connection app-config)
:stop (disconnect app-config conn)) :stop #(disconnect app-config conn))
;; datomic schema (staging as an example) ;; datomic schema (staging as an example)
(defn create-schema [conn] (defn create-schema [conn]

View file

@ -1,7 +1,6 @@
(ns app.www (ns app.www
(:require [app.nyse :refer [add-order find-orders create-nyse-schema]] (:require [app.nyse :refer [add-order find-orders create-nyse-schema]]
[app.config :refer [app-config]] [app.config :refer [app-config]]
[app.utils.logging :refer [with-logging-status]]
[mount.core :refer [defstate]] [mount.core :refer [defstate]]
[cheshire.core :refer [generate-string]] [cheshire.core :refer [generate-string]]
[compojure.core :refer [routes defroutes GET POST]] [compojure.core :refer [routes defroutes GET POST]]
@ -23,13 +22,10 @@
(defn start-nyse [{:keys [www]}] (defn start-nyse [{:keys [www]}]
(create-nyse-schema) ;; creating schema (usually done long before the app is started..) (create-nyse-schema) ;; creating schema (usually done long before the app is started..)
(with-logging-status) ;; enables demo logging
(-> (routes mount-example-routes) (-> (routes mount-example-routes)
(handler/site) (handler/site)
(run-jetty {:join? false (run-jetty {:join? false
:port (:port www)}))) :port (:port www)})))
(declare nyse-app) ;; in case it needs to be accessed in "resume-nyse" (helping out Clojure compiler) (defstate nyse-app :start #(start-nyse app-config)
:stop #(.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
(defstate nyse-app :start (start-nyse app-config)
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point