Fork me on GitHub

Zero config, no server Haste.App

Writing applications using Haste.App has always required a certain amount of manual labour. Installing and configuring a web server to serve the client part is a bit annoying when you’re going to be running a separate server process anyway. Similarly, rebuilding your application with a new configuration whenever you want to deploy it to another host, since your client part will otherwise try to connect to the wrong server, does not exactly feel very modern. It would be so much more convenient if we could compile our application once and have it Just Work(tm) wherever we deploy it.

Fortunately, these problems are not inherent to the Haste.App programming model. As a proof of concept, I’ve hacked up a small tool called haste-standalone, which lets you write applications using Haste.App that

  • don’t require a web server;
  • don’t require any static configuration; and optionally
  • serves any required static files, such as HTML or images, on its own.

The best part? It only requires you to change a single line of code in your application!

haste-standalone by example

The quickest way to explain this newfangled dynamic configuration magic is to contrast it with the old, static configuration. The more or less canonical “Hello, World”-example for traditional Haste.Apps looks as follows:

import Haste.App

main = runApp defaultConfig $ do
  hello <- remote (liftIO $ putStrLn "Hello Server!")
  runClient $ onServer hello

When the client is accessed through a web browser, Hello Server will be printed to the server’s standard output.

Compiling and running this program is quick and easy:

$ ghc --make hello.hs
$ hastec --output-html hello.hs
$ ./hello

Then, you need to set up a web server on your local machine, copy hello.html - the client part of the application - into the web server’s root directory, realize that you don’t have the permissions to do that since the web server’s root directory is owned by www-data or some such user, elevate your privileges using sudo to actually copy over the file, and then navigate to the proper URL using your web browser. God help you if your application also wants to touch the file system, and if you want to deploy your application to another machine then back to square one we go!

OK, I take that back; this is decidedly neither quick nor easy.

Using haste-standalone the same program and procedure looks as follows:

import Haste.App.Standalone

main = runStandaloneApp $ do
  hello <- remote (liftIO $ putStrLn "Hello Server!")
  runClient $ onServer hello

There is only one change here: runApp defaultConfig was replaced by runStandaloneApp. The build process is also quite similar:

$ ghc --make hello.hs
$ hastec hello.hs
$ ./hello --embed=hello.js
$ ./hello
Application started on http://<your IP>:8080

Navigate to http://localhost:8080, and you will see Hello Server printed to standard output in the terminal from which you’re running ./hello. Best of all, if you were to simply drop the hello binary on another machine and run it, it would still work just the same! No more configuring web servers, mucking around with permissions or recompiling your entire application just to switch machines!

The only change in the build process is that we omitted the --output-html option to hastec, and that we added a new call to the server binary, with a mysterious --embed option.

What just happened?

Since haste-standalone aims to enable deployment of web applications by merely dropping an executable on a server, executables built using it must possess some properties that you dont normally get in your old, garden variety Haste.App executables:

  • the executable must contain the client JavaScript, and possibly other data as well;
  • said data needs to be served over HTTP; and
  • the executable must somehow be able to dynamically configure the client JavaScript.

As you can probably deduce from the use of the --embed option, haste-standalone adds some command line argument handling any program that uses runStandaloneApp. Running any such program with the --help flag yields a helpful message describing the available options. Any and all preparation and configuration of standalone Haste.Apps is handled by these built-in facilities.

Preparing the executable

The --embed option prepares a standalone Haste.App for execution. It accepts a JavaScript file as its argument, and interprets all non-option command line arguments as additional files to bake into itself. If an index.html file is included in the additional files, that file will be served when requesting the application’s / URL, effectively becoming the “main page” of the application. Otherwise, the same skeleton HTML code used by Haste’s --output-html option will be served instead.

As an example, the following command line would cause the hello executable to embed a custom main page and a JPEG image itself, in addition to the client JavaScript program. Every file included in this way can be accessed using the file name by which it was embedded into the executable.

$ ./hello --embed=hello.js index.html hello.jpg

Since this causes overwrites the hello executable to attempt to overwrite itself, this will not work on Windows: you can’t normally overwrite an open file. Due to this limitation, haste-standalone is currently not available for Windows systems.

After an executable has been prepared using --embed, any further attempts to re-prepare it will fail, to avoid accidental data loss. This behaviour can be overridden using the --force option.

Serving the client

A web application needs some sort of web server to work, and standalone Haste.Apps are no exception. haste-standalone uses the warp package to serve the client JavaScript as well as any additional files over HTTP.

Since web applications quite often may deal with dynamically created files - think file sharing service - standalone Haste.Apps may also be configured to serve files from a data directory, in addition to the files baked into the executable. The --data-directory option indicates which directory to serve additional files from. The following command line will start the hello executable and configure it to serve additional files from the wwwdata directory.

$ ./hello --data-directory=wwwdata

Any requests for files outside the specified data directory result in a 404 error. If no data directory is given, only files embedded in the executable are served. To avoid accidentally “overwriting” embedded files, if a file is present in both the executable and the data directory, the embedded file is preferred. This behaviour can be reversed using the --override-embedded option.

Dynamic client configuration

However, just serving up static files will not do much for the problem we actually want to solve: getting Haste.Apps to Just Work(tm) in any network environment, independent of any static host or port configuration.

To solve this problem, haste-standalone slightly mangles the client JavaScript program as it is served, appending two configuration variables containing the host and the port on which the application’s server part is running. The process is probably best understood by looking at the client side implementation of runStandaloneApp.

runStandaloneApp :: App Done -> IO ()
runStandaloneApp app = void $ setTimer (Once 0) $ do
  (host, port) <- getHostAndPort
  runApp (mkConfig host port) app

getHostAndPort :: IO (String, Int)
getHostAndPort =
  ffi "(function(){return [window['::hasteAppHost'], window['::hasteAppPort']];})"

On load, the client simply picks up the ::hasteAppHost and ::hasteAppPort configuration variables, so named to avoid clashing with any actual global variables, from the window object, and uses those to construct the Haste.App configuration passed to runApp.

When launching a standalone Haste.App, the host and port on which to run the server are configurable using the --host and --api-port options respectively. Similarly, the HTTP port on which to serve the application’s HTML, JavaScript and other files is configurable using the --http-port option. The following command line will launch the hello application specifically on example.com:8888, using port 12345 for the Haste.App API communication.

$ ./hello --host=example.com --http-port=8888 --api-port=12345
Application started on http://example.com:8888

The HTTP port defaults to 8080 if omitted, and the API port to 24601 (bonus points if you catch the reference). If no host is specified, haste-standalone tries to automatically detect the address of the machine on which it is running. This obviously does not work if your server is behind NAT or in a similar situation, so in this case you will need to manually specify the host on which you want your application to be accessible. This could be improved by moving some of the autodetection to the client, while it’s all on the server for now. This is a slight annoyance, but still much better than having to recompile your application just to move it between hosts.

An aside: why is that code using a zero timeout?

To avoid breaking any "use strict;" declarations, the configuration variables are appended to the JavaScript client rather than prepended. This means that we must wait until the page has finished loading before trying to read them. By default, this does not require any special care to be taken since Haste installs its main function as an onload handler, but if the application was compiled with --onexec this code will run before the code setting the configuration variables!

We could solve this for the --onexec case by installing an onload handler, but if our application was already run from an onload handler, installing a new one will do nothing. Hence, we use a timer with a zero timeout as a form of “yield until everything else is done”, to ensure that applications built both with and without --onexec work out of the box.

Shouldn’t this be in Haste proper?

Going forward, it’s quite likely that some form of support for this will be merged into the Haste compiler. Not least because this would allow Haste to perform the --embed step of the build process instead of having the executable itself do it, which would allow this little trick to work on Windows. However, the extra dependencies added by warp and network-info are not to be taken lightly. If you already have a server environment for your application, you will probably want to go with that instead of the built in server, and in which case all the extra dependencies will seem absolutely ridiculous.

Still, it’s probably workable to support this functionality in Haste proper conditional on the presence of the haste-standalone package. That way, we can both have our cake and eat it, depending on whether we feel like having cake for our next project or not.