Support library for Clojure servers built on the System/Component pattern
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dev
resources/com/mitranim/forge/css
src/com/mitranim
template
.gitignore
project.clj
readme.md

readme.md

Overview

Support library for Clojure servers built on the system/component pattern and Ring.

See example project templates:

Provides plumbing:

  • a place to store the system singleton, safe from namespace reloads (see below)
  • shortcuts for system lifecycle management: recreate, restart

Provides a smooth development experience:

  • automatic code reload on change
  • automatic system reset on code change
  • automatic webpage refresh on system reset
  • rendering of compile errors and runtime exceptions

Inspired by:

  • component.repl: system storage, lifecycle shortcuts
  • lein-ring: automatic code reload, webpage refresh, error rendering

Uses clojure.tools.namespace for code reload. Adapts it to work in background threads such as filesystem watchers. Properly refreshes namespaces in nREPL sessions (assumes clojure.tools.nrepl).

Comparison with component.repl:

  • automatic code reload

  • background code reload correctly refreshes the REPL

  • integrated webserver goodies: auto refresh on system reset, error rendering

Comparison with lein-ring:

  • not Ring-specific

  • doesn't mess with your build, AOT compilation works properly

  • code reload uses clojure.tools.namespace, avoiding limbo states

  • code reload involves a system reset, making it easy to redefine background activities such as job queues

  • background code reload correctly refreshes the REPL

  • code reload works on its own, you don't need a webpage open

  • code reload is vastly more reliable

  • page refresh is vastly more reliable

Installation

Add to project.clj:

[com.mitranim/forge "0.1.0"]

Require in code:

(:require [com.mitranim.forge :as forge])

Usage

(ns core
  (:gen-class)
  (:require
    [com.mitranim.forge :as forge]
    [com.stuartsierra.component :as component]))

(defn create-system [prev-system]
  (reify
    component/Lifecycle
    (start [this] (println "starting") this)
    (stop [this] (println "stopping") this)))

(defn -main []
  (forge/reset-system! create-system))

(defn -main-dev []
  (forge/start-development! {:system-symbol `create-system})
  (forge/reset-system! create-system))

When using Ring, add the middleware that automatically refreshes webpages and renders errors:

(let [my-ring-handler (forge/wrap-development-features my-ring-handler)])

Launch your REPL and run an equivalent of this:

(forge/start-development! {:system-symbol `create-system})
(forge/reset-system! create-system)
forge/sys

Now, modifying source files or running (forge/reset) will trigger a code reload and system reset. The current system is always stored in forge/sys.

Enjoy your workflow!

The template folder in this repo provides the absolute smallest starting core. Copy it to start playing around.

API

The most important stuff is listed here. To dig deeper, check the source. It's simple and hackable.

All functions here are thread-safe and idempotent.

set-system-symbol!

Tells Forge where to find your create-system function after a namespace refresh. Needs to be called once before using reset. start-development! also sets this.

(forge/set-system-symbol! `create-system)
forge/system-symbol

sys

Stores the current system. Gets modified by reset and reset-system!. Can be used to avoid passing the system everywhere. Also convenient in the REPL.

(forge/reset)
forge/sys

start-development!

Starts auto-reload and other goodies. Run it once after launching the REPL. See Usage for example code.

reset

Reloads modified namespaces, recreates and restarts the system. Must be called after set-system-symbol! or start-development!.

After one start-development! call, reset runs automatically on every source change.

; once
(forge/start-development! {:system-symbol `create-system})
(forge/reset)

reset-system!

Recreates and restarts the system, storing the result in #'forge/sys. In development, use reset instead of this.

Define your "create-system" function. It must take one argument, the previous version of the system, and return the next version without starting it.

Handles exceptions carefully:

  • exception when stopping → store the partially stopped system so you can fix it manually

  • exception when starting → stop the partially started system, store the remainder

The latter can be convenient when debugging production failures. If any component fails to start, the rest won't keep the JVM from shutting down.

(defn create-system [prev-system]
  (component/system-map))

(defn -main []
  (try (forge/reset-system! create-system)
    (catch Throwable err
      (shutdown-agents)
      (binding [*out* *err*] (prn err))
      (System/exit 1))))

wrap-development-features

Optional Ring middleware for auto-refresh and error rendering. Add to your middleware stack as an outer layer, typically just before the 500 handler:

(def handler
  (-> routes
      ... other middleware ...
      forge/wrap-development-features
      my-500-handler))

Running restart-system! or reset will refresh any open webpages.

refresh-namespaces

Refreshes any modified namespaces. This is a version of clojure.tools.namespace.repl/refresh that works in background threads, so it's usable in filesystem watchers, HTTP handlers, etc. Used internally by reset.

(forge/refresh-namespaces)

; works
(future (forge/refresh-namespaces))

Note: unlike (require 'my-ns :reload), this completely replaces namespace objects, breaking defonce. To preserve state, keep it in your System and migrate between system resets. If you have sufficiently good reasons, you can opt a namespace out of "hard" reload into "soft" reload:

(ns my-ns
  (:require
    [clojure.tools.namespace.repl :refer [disable-unload!]]))

(disable-unload!)

(defonce blah blah)

...

For lower-level stuff, please run (dir com.mitranim.forge) and check the source; it's annotated and self-explanatory.

Changelog

0.1.2

Avoid double status notification on reset.

0.1.1

More reliable webpage reloading. Now uses websockets to avoid a few edge cases in long polling.

0.1.0

init

Misc

Feedback, criticism, suggestions, and pull requests are welcome!

Open an issue or reach me on skype:mitranim.web or me@mitranim.com.