{"id":20576496,"url":"https://github.com/zalky/runway","last_synced_at":"2025-04-14T18:22:46.343Z","repository":{"id":65595507,"uuid":"536247304","full_name":"zalky/runway","owner":"zalky","description":"Coding on the fly, from take-off to landing, with a tools.deps reloadable build library","archived":false,"fork":false,"pushed_at":"2023-02-15T02:59:49.000Z","size":63,"stargazers_count":21,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-28T06:51:10.716Z","etag":null,"topics":["clojure","component","live-coding","repl","tools-deps"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zalky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-09-13T17:54:10.000Z","updated_at":"2023-09-11T05:00:20.000Z","dependencies_parsed_at":"2023-02-17T08:46:16.744Z","dependency_job_id":null,"html_url":"https://github.com/zalky/runway","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalky%2Frunway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalky%2Frunway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalky%2Frunway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalky%2Frunway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zalky","download_url":"https://codeload.github.com/zalky/runway/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248933718,"owners_count":21185531,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","component","live-coding","repl","tools-deps"],"created_at":"2024-11-16T05:45:58.986Z","updated_at":"2025-04-14T18:22:46.127Z","avatar_url":"https://github.com/zalky.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://i.imgur.com/GH71uSi.png\" title=\"zalky\" align=\"right\" width=\"250\"/\u003e\n\n# Runway\n\n[![Clojars Project](https://img.shields.io/clojars/v/io.zalky/runway?labelColor=blue\u0026color=green\u0026style=flat-square\u0026logo=clojure\u0026logoColor=fff)](https://clojars.org/io.zalky/runway)\n\nCoding on the fly, from take-off to landing, with a tool.deps reloadable\nbuild library.\n\nWith this library:\n\n1. Power up your `deps.edn` aliases:\n   - [Run multiple concurrent functions](#concurrent) via merged deps\n     aliases in the same runtime\n   - A simple way to [load extra namespaces](#load-ns)\n   - Merge in [environment variables](#env-vars) (not enabled by\n     default, opt-in via peer dependency)\n\n2. Enjoy [rock-solid live reloading](#reload) of code and lifecycle\n   management of your running application:\n   - Uses a fork of\n     [`clojure.tools.namespace`](https://github.com/zalky/tools.namespace)\n     (c.t.n.) that fixes\n     [`TNS-6`](https://clojure.atlassian.net/browse/TNS-6), which\n     greatly improves c.t.n robustness (a patch has been submitted and\n     approved)\n   - The provided implementation is for a `com.stuartsierra.component`\n     system. However it can be [extended for any arbitrary build\n     framework](#other-framework).\n   - Choose how to reload dependent namespaces: eagerly or lazily\n   - Uses the new cross-platform [Axle](https://github.com/zalky/axle)\n     watcher (performs much better on newer Macs and newer versions of\n     Java)\n   - More robust error handling, recovery and logging during component\n     lifecycle methods\n   - Cleanly shutdown your application on interrupt signals\n\n### About Reloaded Workflows\n\n[Reloaded workflows](https://cognitect.com/blog/2013/06/04/clojure-workflow-reloaded)\ncan be difficult to implement and there are a number of\n[known pitfalls](https://github.com/clojure/tools.namespace#warnings-and-potential-problems)\nwith when using `clojure.tools.namespace`.\n\nHowever, some of these can be mitigated, and others are not specific\nto reloaded workflows and are things that you need to worry about in\nany live coding environment. Meanwhile, the benefits that reloaded\nworkflows bring are significant, especially when live coding alongside\nlarge, running applications. Having automated, enforced heuristics for\nhow an application behaves as your code changes allows you to a priori\neliminate a whole subset of failure points. And these failure points\nare often much trickier than the known gotchas of reloaded code.\n\nSo if you're like me, and think reloaded workflows are more than worth\nthe effort, Runway provides a rock-solid component-based\nimplementation to do it.\n\n## Contents\n\n1. [Quick Start](#quick-start)\n2. [Concurrent Functions](#concurrent)\n   - [Writing Concurrent Functions](#concurrent-functions)\n3. [Loading a Namespace](#load-ns)\n4. [Environment Variables](#env-vars)\n5. [Reloaded Workflow](#reload)\n   - [Configuration](#reload-config)\n   - [Auto REPL Configuration](#repl-auto-setup)\n   - [Source Directories](#source)\n   - [Reload Heuristics](#reload-heuristics)\n   - [Component Lifecycles](#lifecycles)\n   - [Error Handling and Recovery](#errors)\n   - [Logging](#logging)\n6. [System Definition](#system-def)\n   - [Example System](#example-system)\n   - [System Component Library](#system-components)\n7. [Main Invocation](#main)\n8. [Other Build Frameworks](#other-framework)\n9. [License](#license)   \n\n## Quick Start \u003ca name=\"quick-start\"\u003e\u003c/a\u003e\n\nLet's say you want to start two concurrent tasks in the same runtime:\nan nREPL server and a code watcher. Just put the following in your\n`deps.edn` file:\n\n```clj\n{:deps    {io.zalky/runway {:mvn/version \"0.2.2\"}}\n :paths   [\"src\"]\n :aliases {:repl    {:extra-deps {nrepl/nrepl {:mvn/version \"0.8.3\"}}\n                     :exec-fn    runway.core/exec\n                     :exec-args  {runway.nrepl/server {}}}\n           :watcher {:exec-fn    runway.core/exec\n                     :exec-args  {runway.core/watcher {}}}}}\n```\n\nThen you can then run either a single task:\n\n```\nclojure -X:repl\n```\n\nOr both concurrently:\n\n```\nclojure -X:repl:watcher\n```\n\nWith normal `-X` invocation, only one function is ever run. However\nwhen that function is `runway.core/exec`, it collects and runs any\nnumber of _other_ functions defined in your aliases via merge\nsemantics. Combined with other Runway features this provides a modular\nand flexible approach to what is executed in your runtime.\n\nNote that this minimal example does not start a running application\nfor you to live code along-side. The next section explains how to do\nthat.\n\nAlso, make sure you have read the [simple guidelines](#reload) on how\nto make your code reloading experience more robust. But TL;DR:\n\n1. Do not move your REPL into a namespace backed by a Clojure file on\n   the classpath, ex: `(in-ns 'ns.backed.by.my.clojure.file)`\n\n2. Instead require and alias any namespaces you want to use in your\n   REPL namespace\n\n3. No AOT compile, no defonce\n\nAdditionally, if you use Cider you might want to include some nREPL\nmiddleware in your `:repl` alias dependencies:\n\n```clj\ncider/cider-nrepl {:mvn/version \"0.28.5\"}   ; or whatever your cider version is\nrefactor-nrepl/refactor-nrepl {:mvn/version \"3.5.5\"}\n```\n\n## Concurrent Functions \u003ca name=\"concurrent\"\u003e\u003c/a\u003e\n\nRunway provides a means to run multiple concurrent functions via deps\naliases. This is mostly useful when these concurrent functions need\naccess to the same runtime, otherwise you would just run them as\nseparate processes. Lets say you want to start a\n`com.stuartsierra.component` based application server, an nREPL\nserver, and a file watcher to reload code. Runway already provides\nthree built-in functions that do this for you.\n\nJust configure a `deps.edn` that looks like the following:\n\n```clj\n{:deps    {io.zalky/runway {:mvn/version \"0.2.2\"}}\n :paths   [\"src\"]\n :aliases {:dev    {:extra-deps {nrepl/nrepl {:mvn/version \"0.8.3\"}}\n                    :exec-fn    runway.core/exec\n                    :exec-args  {runway.nrepl/server {}\n                                 runway.core/watcher {}}}\n           :server {:exec-fn   runway.core/exec\n                    :exec-args {runway.core/go {:system my.project/app}}}}}\n```\n\nThe `:exec-args` of each alias define a set of function symbol and\nargument pairs. Here, the `:dev` alias defines both an nREPL server\nand a code watcher task, and the `:server` alias defines an\napplication server. If you now run:\n\n```\nclojure -X:dev:server\n```\n\nClojure will first merge those aliases according to the normal\nsemantics of -X invocation, and then pass their combined `:exec-args`\nmap to `runway.core/exec`. Runway will then locate the functions\ndeclared in the combined `:exec-args` maps, load their namespaces, and\nrun each of them concurrently with their respective arguments.\n\nEffectively the alias that gets run is:\n\n```clj\n{:extra-deps {nrepl/nrepl {:mvn/version \"0.8.3\"}}\n :exec-fn    runway.core/exec\n :exec-args  {runway.nrepl/server {}\n              runway.core/watcher {}\n              runway.core/go      {:system my.project/app}}}\n```\n\nEasy. Your function aliases are composable in any combination without\nhaving to re-write them, and their `:exec-args` are merged in the\norder in which you invoked your aliases.\n\nAny truthy function argument like `{}` or `:arg` will be passed along\nto the function, whereas any falsy value indicates that the function\nshould not be run. Take the alias:\n\n```clj\n{:watcher/disable {:exec-fn   runway.core/exec\n                   :exec-args {runway.core/watcher false}}}\n```\n\nThis can be used to disable the watcher in other aliases. The\nfollowing will be merged in order:\n\n```\nclojure -X:dev:server:watcher/disable\n```\n\nThere is also the option to merge in `:exec-args` via command line\narguments. The following will merge the `:exec-args` of the two\naliases `:dev` and `:server`, along with the command line edn:\n\n```\nclojure -X:dev:server '{my.project/my-process {:my \"cli_arg\"}}'\n```\n\nThe effective alias that gets run is:\n\n```clj\n{:extra-deps {nrepl/nrepl {:mvn/version \"0.8.3\"}}\n :exec-fn    runway.core/exec\n :exec-args  {runway.nrepl/server   {}\n              runway.core/watcher   {}\n              runway.core/go        {:system my.project/app}\n              my.project/my-process {:my \"cli_arg\"}}}\n```\n\nTo see debugging information about what namespaces, functions,\n`:exec-args`, and env variables are being merged in by Runway, simply\nadd `:verbose \"true\"` to your `:exec-args`:\n\n```\nclojure -X:dev:server '{my.project/my-process {:my \"cli_arg\"} :verbose \"true\"}'\n\n22-09-27 16:19:45 zalky INFO [runway.core:488] - No environ.core, skipping\n22-09-27 16:19:45 zalky INFO [runway.core:495] - Loaded namespaces (runway.nrepl runway.core my.project)\n22-09-27 16:19:46 zalky INFO [runway.core:496] - Exec fns found (runway.nrepl/server runway.core/watcher runway.core/go my.project/my-process)\n22-09-27 16:19:46 zalky INFO [runway.core:497] - Exec args {runway.nrepl/server {}, runway.core/watcher {}, runway.core/go {:system my.project/app} my.project/my-process {:my \"cli_arg\"}}\n```\n\nNote the string quotes around `:verbose \"true\"`. We'll come back to\nthat later.\n\n### Writing Concurrent Functions \u003ca name=\"concurrent-functions\"\u003e\u003c/a\u003e\n\nRun functions can be defined anywhere in your code. If your function\nis a long-running concurrent process that needs the main thread to\nblock and stay alive, then it should return a response map:\n\n```clj\n{:runway/block true}\n```\n\nOnce all processes have been launched, Runway will print a boot time:\n\n```\n22-09-26 03:43:40 zalky INFO [runway.core:132] - Starting my.project/app\n22-09-26 03:43:40 zalky INFO [runway.build:18] - Transitive dependency started\n22-09-26 03:43:40 zalky INFO [runway.build:27] - Dependency started\n22-09-26 03:43:40 zalky INFO [runway.build:36] - Dependent started\n22-09-26 03:43:40 zalky INFO [runway.build:9] - Singleton started\n22-09-26 03:43:40 zalky INFO [runway.core:354] - Watching system...\n22-09-26 03:43:40 zalky INFO [runway.nrepl:72] - nREPL server started on port 50929 on host localhost - nrepl://localhost:50929\n22-09-26 03:43:40 zalky INFO [runway.core:490] - Boot time: 2.70s\n```\n\nIf you return a `:runway/ready` promise in your response map, the boot\ntime will not print until your promise has been delivered. For\nexample, you could define a function like so:\n\n```clj\n(ns my.project)\n\n(defn my-process\n  [exec-args]\n  (let [ready (promise)]\n    (future\n      (init-process! exec-args)\n      (deliver ready true)\n      (run-process! exec-args))\n    {:runway/block true\n     :runway/ready ready}))\n```\n\nSee the `runway.nrepl/server` function for a real example. You could\nthen configure your function in an alias:\n\n```clj\n{:my-process {:exec-fn    runway.core/exec\n              :exec-args  {my.project/my-process {:my \"arg\"}}}}\n```\n\nAnd invoke it with:\n\n```clj\nclojure -X:dev:server:my-process\n```\n\n## Loading a Namespace \u003ca name=\"load-ns\"\u003e\u003c/a\u003e\n\nQualified symbols are interpreted as a run functions by\n`runway.core/exec`. _Unqualified_ symbols are interpreted as\nnamespaces to load. Given:\n\n```clj\n{:deps    {io.zalky/runway {:mvn/version \"0.2.2\"}}\n :paths   [\"src\"]\n :aliases {:server {:exec-fn   runway.core/exec\n                    :exec-args {runway.core/go   {:system my.project/app}\n                                my.project.other true}}}}\n```\nThen\n\n```\nclojure -X:server\n```\n\nWill run the `runway.core/go` function as well as load the\n`my.project.other` namespace, presumably for side-effects.\n\nAs always, to see debugging information about what namespaces are\nbeing loaded by Runway, use `:verbose \"true\"`:\n\n```\nclojure -X:server '{:verbose \"true\"}'\n```\n\n## Environment Variables \u003ca name=\"env-vars\"\u003e\u003c/a\u003e\n\n[`Environ`](https://github.com/weavejester/environ) is a library that\ncan import environment settings from a number of different sources,\nincluding environment variables.\n\nOne thing to be aware of when using Environ is that it loads _all_\nyour environment variables, including potentially sensitive ones, into\nmemory and stores them in `environ.core/env`. For this reason, Runway\ntreats Environ as a peer dependency.\n\nIf you do not include Environ as a dependency in your `deps.edn` (and\nit is not available on the classpath), then no variables are loaded\nand all the environment features in this section will be ignored.\n\nIf you do include it, Runway provides you with a way to merge\nadditional env variables into `environ.core/env` using the\n`:exec-args` maps in your `deps.edn` aliases:\n\n```clj\n{:deps    {io.zalky/runway {:mvn/version \"0.2.2\"}\n           environ/environ {:mvn/version \"1.2.0\"}}\n :paths   [\"src\"]\n :aliases {:dev         {:extra-deps {nrepl/nrepl {:mvn/version \"0.8.3\"}}\n                         :exec-fn    runway.core/exec\n                         :exec-args  {runway.nrepl/server {}\n                                      runway.core/watcher {}}}\n           :server      {:exec-fn   runway.core/exec\n                         :exec-args {runway.core/go {:system my.project/app}\n                                     :my-env-var-1  \"val1\"}}\n           :server/opts {:exec-args {:my-env-var-2 \"val2\"}}}}\n```\n\nAny keywords in your `:exec-args` maps are interpreted as env\nvariables that should be merged into your `environ.core/env` map. If\nyou now invoke your programs with:\n\n```\nclojure -X:repl:server:server/opts\n```\n\nYou should have access to both `:my-env-var-1` and `:my-env-var-2` in\n`environ.core/env`:\n\n```clj\n(require '[environ.core :as env])\n\n(select-keys env/env [:my-env-var-1 :my-env-var-2])\n=\u003e\n{:my-env-var-1 \"val1\", :my-env-var-2 \"val2\"}\n```\n\nYour alias variables override any values that may have been exported\ndirectly in your environment. For example, let's say we remove the\n`:server/opts` alias and invoke just:\n\n```\nclojure -X:repl:server\n```\n\nThen the value of `:my-env-var-1` will always be `\"val1\"` (as defined\nin `:server`) no matter what is `export`ed by your environment, but\nthe value of `:my-env-var-2` would depend whether or not you had\n`export`ed it as an env VARIABLE:\n\n```\nexport MY_ENV_VAR_2=env_val\n```\n\nThe above mechanism provides you with an easy way to configure your\napplication across development and production environments.\n\nTo see debugging information about what env variables are being merged\nin by your edn, use `:verbose \"true\"` (this will _not_ print the full\nset of env VARIABLES loaded by from your environment, just those in\nedn):\n\n```\nclojure -X:repl:server:server/opts '{:verbose \"true\"}'\n\n22-09-27 17:15:30 zalky INFO [runway.core:494] - Merged env {:my-env-var-1 \"val1\", :my-env-var-2 \"val2\", :verbose \"true\"}\n22-09-27 17:15:31 zalky INFO [runway.core:495] - Loaded namespaces (runway.nrepl runway.core)\n22-09-27 17:15:31 zalky INFO [runway.core:496] - Exec fns found (runway.nrepl/server runway.core/watcher runway.core/go)\n22-09-27 17:15:31 zalky INFO [runway.core:497] - Exec args {runway.nrepl/server {}, runway.core/watcher {}, runway.core/go {:system my.project/app}}\n22-09-27 17:15:31 zalky INFO [runway.core:377] - Watching system...\n22-09-27 17:15:31 zalky INFO [runway.core:137] - Starting my.project/app\n```\n\nOf course, you can merge in variables via your cli `:exec-args`:\n\n```\nclojure -X:repl:server:server/opts '{:verbose \"true\" :my-cli-var \"other\"}'\n22-09-27 17:24:35 zalky INFO [runway.core:494] - Merged env {:my-env-var-1 \"val1\", :my-env-var-2 \"val2\", :verbose \"true\", :my-cli-var \"other\"}\n```\n\n### Env Variable Values Must Be Strings\n\n`:exec-args` are necessarily parsed as edn, and therefore so are the\nvalues in your `:exec-args` maps. However, env variables that you\nexport in your environment are not:\n\n```\nexport MY_CLI_VAR=false\n```\n\nThe above will be loaded as the string `\"false\"`, which is actually a\ntruthy value in your running application. Therefore to preserve the\nsemantics of environment variables, Runway will throw an error if you\ntry to pass a non-string value to an `:exec-args` env var:\n\n```\nclojure -X:repl:server:server/opts '{:my-cli-var false}'\nException in thread \"main\" java.lang.Exception: :exec-args environment key :my-cli-var error: value false must be a string\n```\n\nThis is why we have been passing `:verbose \"true\"` throughout these\nexamples, and not `:verbose true`.\n\n## Reloaded Workflow \u003ca name=\"reload\"\u003e\u003c/a\u003e\n\nReloaded workflows with Runway can be extremely robust. Three simple\nguidelines are all you need to help mitigate pitfalls:\n\n1. Do not move your REPL into a namespace backed by a Clojure file on\n   the classpath. Namespaces backed by files are constantly being\n   replaced by c.t.n., and if you move your REPL into one like so:\n\n   ```clj\n   (in-ns 'ns.backed.by.my.clojure.file)\n   ```\n\n   You will find it hard to persist your REPL vars across\n   reloads. Whereas, by staying in a namespace that is not backed by a\n   file, and therefore not reloaded, your REPL vars will be preserved.\n\n   While you work, Runway will keep your aliases and refers your REPL\n   namespaces consistent with the changing namespace graph.\n\n2. Instead require and alias any namespace you want to use in your\n   REPL. If you find yourself regularly requiring a common set of\n   namespaces see here for [how to automate this](#repl-auto-setup).\n\n3. No AOT compile, no defonce\n\n   While you can use defonce to define vars, because the entire\n   namespaces gets replaced by c.t.n., it can't protect those vars\n   from being redefined.\n\nNext, some caveats about live-coding that are not specific to reloaded\nworkflows, but are nevertheless critical to be aware of:\n\n1. Be careful what you `def` in your REPL namespace. _Especially\n   objects that implement protocols or interfaces, like system\n   components!_ Anything you `def` into your REPL becomes a snapshot\n   of your code at a point in time, and can easily get out of sync as\n   your other namespaces change. And while Runway updates your REPL\n   aliases and refers, it cannot account for stale REPL state that you\n   may have captured through `def`s or closures.\n\n   If you are not sure whether the thing you are `def`ing into your\n   REPL can become stale, then consider using a `defn` instead.\n\n2. `defmethod`s will be updated, but cannot be outright removed. If\n   you really want a stale `defmethod` gone, the fool-proof approach\n   is to trigger a reload on the file that contains the `defmulti`\n   MultiFn. `clojure.core/remove-method` can also work, but there are\n   some edge cases if you also use `clojure.core/prefer-method`.\n\n3. Runway's code reloading is robust enough that you should be able to\n   switch git branches that are any distance apart, as long as the\n   classpath doesn't change. However, any changes with implications on\n   the classpath are likely to cause problems.\n\n### Configuration \u003ca name=\"reload-config\"\u003e\u003c/a\u003e\n\nTypically you would start at least a development server, a code\nwatcher, and an nREPL server. Something like this in your `deps.edn`\naliases would work:\n\n```clj\n{:dev    {:extra-deps {nrepl/nrepl       {:mvn/version \"0.8.3\"}\n                       cider/cider-nrepl {:mvn/version \"0.28.5\"}             ; optional\n                       refactor-nrepl/refactor-nrepl {:mvn/version \"3.5.5\"}} ; optional\n          :exec-fn    runway.core/exec\n          :exec-args  {runway.nrepl/server {}\n                       runway.core/watcher {}}}\n :server {:exec-fn   runway.core/exec\n          :exec-args {runway.core/go {:system my.project/app}}}}\n```\n\nYou could start any combination of the above aliases from the command\nline:\n\n```\nclojure -X:dev:server\n```\n\n#### `runway.core/watcher`\n\nWatches your Clojure files, reloads their corresponding namespaces\nwhen they change, and then if necessary, restarts the running\napplication. It is configurable with the following options:\n\n1. **`:watch-fx`**: accepts a sequence of `runway.core/do-fx`\n   multimethod dispatch values. See `runway.core/watcher-config` for\n   the default list. You can extend watcher functionality via the\n   `runway.core/do-fx` multimethod. Each method behaves like an\n   interceptor: it accepts a c.t.n. tracker map and returns and\n   updated one, potentially modifying the remaining fx chain. Just\n   take care: the order of fx methods is not necessarily commutative.\n\n2. **`:lazy-dependents`**: whether to load dependent namespaces\n   eagerly or lazily. See [the section on reloading\n   heuristics](#reload_heuristics) for more details.\n\n3. **`:restart-fn`**: A symbol to a predicate function that when given\n   a c.t.n. tracker and a namespace symbol, returns a boolean whether\n   or not the system requires a restart due to the reloading of that\n   namespace. This allows you to override the default logic of when\n   the running application is restarted. The defualt is to restart the\n   application whenever a direct dependent of\n   `com.stuartsierra.component` is reloaded.\n\n4. **`:restart-paths`**: A list of paths that the watcher should\n   monitor for changes to trigger an application restart. Note that\n   the paths can be either files or entire directories. This is useful\n   if your reloaded workflow depends on static resources that are not\n   Clojure code, but may affect the running application. For example,\n   let's say your application is configured via edn in a `config/edn`\n   directory:\n\n   ```\n   {:exec-fn   runway.core/exec\n    :exec-args {runway.core/watcher {:restart-paths [\"config/edn\"]}}}\n   ```\n\n#### `runway.core/go`\n\nStarts the application once on boot (after boot, you can start or stop\nthe application manually or via the watcher). It has the following\noptions:\n\n1. **`:system`**: A symbol that refers to a function that when called\n   with no arguments, returns a `com.stuartsierra.component/SystemMap`\n   ([see here on how to extend for other build\n   frameworks](#other-framework)). In the example above it would be a\n   function called `my.project/app`.\n\n1. **`:shutdown-signals`**: A sequence of strings representing POSIX\n   interrupt signals (default is `[\"SIGINT\" \"SIGTERM\" \"SIGHUP\"]`). On\n   receiving such a signal Runway will first attempt to shutdown the\n   application before re-raising.\n\n#### `runway.nrepl/server`\n\nLaunches an nREPL server for you to connect to, and has the following\noptions:\n\n1. **`:port`**: nREPL port where to listen to for connections. For\n   example you could configure a specific port directly from the\n   command line like so:\n\n   ```clj\n   clojure -X:dev:server '{runway.nrepl/server {:port 50000}}'\n   ```\n\n2. **`:middleware`**: A list of nREPL middleware symbols. Each symbol\n   can either directly reference a middleware function, or point to a\n   list of more symbols. For example:\n\n   ```clj\n   {runway.nrepl/server {:middleware [my.project.nrepl/my-middleware-list]}}\n   ```\n\n   `cider.nrepl` and `refactor-nrepl.middleware` is special: if no\n   `:middleware` option is provided, but either is on the classpath,\n   then their default middleware sets are loaded automatically. So all\n   you have to do is include `cider/cider-nrepl` or\n   `refactor-nrepl/refactor-nrepl` in your deps, and they should\n   work. See `runway.nrepl/default-middleware` for the full list of\n   default middleware.\n\n### Auto REPL Configuration \u003ca name=\"repl-auto-setup\"\u003e\u003c/a\u003e\n\nIf you find yourself regularly requiring and aliasing a common set of\nnamespaces simply set up a namespace like so:\n\n```clj\n(ns dev.repl) ; The namespace for this file, NOT your REPL namespace\n\n(ns user)     ; Here is your REPL namespace\n\n(require '[my.project.admin :as admin]\n         '[my.project.auth :as auth]\n         '[my.project.session :as session]\n         '[my.project.comms :as comms]\n         '[my.project.s3 :as s3]\n         '[clojure.java.io :as io]\n         '[clojure.string :as str]\n         '[clojure.set :as set]\n         '[datomic.api :as d]\n         '[cinch.core :as util]\n         '[runway.core :as run]\n         '[taoensso.timbre :as log]\n         '[taoensso.nippy :as nippy])\n```\n\nThe key thing here is that the location of this file on the classpath,\nfor example `\u003cclasspath\u003e/dev/repl.clj`, does not map to your REPL\nnamespace. So if your REPL namespace is `user`, you do not want this\nfile to be `\u003cclasspath\u003e/user.clj`. You want to make sure that your\nREPL namespace `user` is not backed by a file, so that it plays well\nwith c.t.n.\n\nThen ensure this namespace is loaded by the appropriate `deps.edn`\nalias:\n\n```clj\n{:dev {:extra-paths [\"path/to/\"]         ; \u003c- path to dev/repl.clj\n       :extra-deps  {nrepl/nrepl                   {:mvn/version \"0.8.3\"}\n                     cider/cider-nrepl             {:mvn/version \"0.28.5\"} ; optional\n                     refactor-nrepl/refactor-nrepl {:mvn/version \"3.5.5\"}} ; optional\n       :exec-fn     runway.core/exec\n       :exec-args   {runway.nrepl/server {}         ; concurrent run fn\n                     runway.core/watcher {}         ; concurrent run fn\n                     dev.repl            true}}}    ; \u003c- ns loaded here\n```\n\nWhen working with a team, you'll usually want to put your REPL config\nin your personal `~/.clojure/deps.edn`, because REPL workflows are\nfairly user specific. But given the flexibility Runway gives you to\nmerge and run aliases, you have an endless number of ways to do so:\n\n```clj\n{:repl/config        {:extra-paths [\"path/to/\"]\n                      :exec-fn     runway.core/exec\n                      :exec-args   {dev.repl true}}\n :repl/datomic       {:exec-fn   runway.core/exec\n                      :exec-args {dev.repl.db true}}\n :repl/elasticsearch {:exec-fn   runway.core/exec\n                      :exec-args {dev.repl.elasticsearch true}}\n ...}\n```\n\nNote that if you update the above `dev.repl` config namespace, the\nwatcher will see this, reload it, and automatically reconfigure your\nREPL. But remember, this approach is _only_ for setting up your REPL\nenvironment. The evaluation of `dev.repl` should not produce stateful\nside-effects in your actual application. This is what\n`com.stuartsierra.component` is for.\n\n### Source Directories \u003ca name=\"source\"\u003e\u003c/a\u003e\n\nAny directories on your classpath will be searched for Clojure files\nby the Runway watcher. Use the `:paths` in your `deps.edn` aliases to\ndetermine what is watched.\n\n### Reload Heuristics \u003ca name=\"reload-heuristics\"\u003e\u003c/a\u003e\n\nFirst, to trigger a code reload you actually have to change the\ncontents of a file. Simply saving a file without modifying its\ncontents will do nothing.\n\nWhile live coding you can usually rely on runway to at minimum reload\nyour changed namespaces in dependency order, and additionally reload\nthe transitive dependents of those changed namespaces. However, the\nbehaviour of transitive dependents loading can be configured to be\neither eager (the default), or lazy.\n\nTo understand what this means, we first have to understand the\ndifference between the namespace dependency graph that\n`clojure.tools.namespace` computes from the contents of your source\nfiles, and the actual dependency graph between your namespace objects\nin memory.\n\nWhen you first load your application with the `runway.core/go` task,\nonly the subset of the code that is required to construct your\napplication is loaded into memory. Likewise, if you start just a REPL,\nonly those namespaces that you `require` in your REPL will be\nloaded. But once you start a `runway.core/watcher` task, and by\nextension a `clojure.tools.namespace` tracker, they will automatically\nreload _all_ dependents of a changed namespace that are found\n_anywhere in your source directories_, including dependents that have\nuntil that point not been required either by the running application\nor your REPL.\n\nFor example, let's say you've defined your running application in\nnamespace `b`, which has a single dependency `a`.\n\n```\n  a\n /\nb     ; b defines your running app and requires a\n```\n\nThere's another source file, `c`, that is not required for your\nrunning application, but also requires `a`.\n\n```\n  a\n / \\\nb   c\n```\n\nWhen you start your app with `runway.core/go`, and a\n`runway.core/watcher` task, initially only `a` and `b` are loaded into\nmemory.\n\nHowever as soon as you modify `a` on disk, both dependents `b` and `c`\nare eagerly reloaded by the watcher, even though `c` was not initially\nrequired by your running application. The heuristics are the same if\nyou start a REPL and a watcher, evaluate `(require 'b)`, and then\nmodify `a` on disk: `c` will be eagerly reloaded.\n\nThe idea is that for any change to source, you want to realize _all_\ndependent effects right away. You do not want to wait for changes to\naccumulate, only to later find out that conflicts or errors have been\nintroduced and have become more difficult to resolve. So the eager\nrealization of dependent source changes is almost always desirable.\n\nHowever, it may be that loading namespace `c` produces some\nside-effects that you'd rather not have happen (side-effects in your\nnamespaces are best avoided, but such is life). If you really need,\nyou can configure the Runway watcher to load dependents lazily via the\n`:lazy-dependents` option.\n\nWith `:lazy-dependents true`, the watcher would not automatically\nreload `c` unless it has already been loaded once by some other means,\neither as part of the application boot, or explicitly from the REPL\nwith `(require 'c)`. From that point on, any changes to `a`, would\nautomatically cause `c` to reload.\n\n### Component Lifecycles \u003ca name=\"lifecycles\"\u003e\u003c/a\u003e\n\nJust before reloading code, Runway also checks whether any of the\nnamespaces it is about to update depends directly on\n`com.stuartsierra.component`. These namespaces usually define system\ncomponents that implement the `com.stuartsierra.component/Lifecycle`\nprotocol and participate in the running application. Before reloading\nany such namespaces, Runway will first stop the running\napplication. Then if all namespaces are successfully reloaded, Runway\nwill start the running application again. This process ensures that\nall the stateful objects that were loaded as part of your running\napplication are consistent with your updated code.\n\nAt any point, you can manually stop, start or restart your running\napplication from the REPL with:\n\n```clj\n(require '[runway.core :as run])\n\n(run/stop)\n(run/start)\n(run/restart)\n```\n\nIf you need access to your running application during live coding, it\nresides in the `runway.core/system` var. But remember, never access\nthis var or its contents outside of your REPL work. For example, you\nnever want to require or access this var in your application\nnamespaces: doing so by-passes the component dependency graph and is\nconsidered a component anti-pattern.\n\n### Error Handling and Recovery  \u003ca name=\"errors\"\u003e\u003c/a\u003e\n\nRunway handles errors in each phase of a reload in different ways:\n\n1. **Component failed `start` lifecycle**: Runway will abort the\n   start, and then attempt to recover by stopping the components of\n   the system it had already started up to that point.\n\n2. **Component failed `stop` lifecycle**: Runway will abort stopping\n   the problem component, but attempt to recover by stopping all other\n   components that are _not_ transitive dependents of the problem\n   component (transitive dependents should already have been stopped).\n\n3. **Namespace reloading failed**: Runway will not restart the running\n   application until all namespace compile errors have been resolved.\n\n### Logging \u003ca name=\"logging\"\u003e\u003c/a\u003e\n\nRunway implements logging via the excellent\n[`com.taoensso/timbre`](https://github.com/ptaoussanis/timbre) library\nfor maximum extensibility.\n\nSpecifically, Lifecycle exceptions are logged out with full component\nand system data. However, because components in complex systems can be\nquite large, you may want to truncate such output to avoid spamming\nterminals and log sinks. You can easily do so using custom timbre\nappenders.\n\n## System Definition \u003ca name=\"system-def\"\u003e\u003c/a\u003e\n\nYou can make your `com.stuartsierra.component/SystemMap` any way you\nlike, but Runway also provides some facilities that make it a bit\neasier:\n\n```clj\n(ns my.project\n  (:require [runway.core :as run]))\n\n(def base-components\n  {:dependency [-\u003eDependency arg1 arg2]\n   :dependent  [-\u003eDependent]})\n\n(def base-dependencies\n  {:dependent [:dependency]})\n\n(defn base-system\n  \"Symbol that gets passed to runway.core/go\"\n  []\n  (run/assemble-system base-components base-dependencies))\n```\n\nHere `-\u003eDependency` is any constructor function that when applied to\nits arguments `arg1 arg2`, returns a component that implements the\n`com.stuartsierra.component/Lifecycle` protocol.\n\nDefined as such, you can easily re-combine systems according to merge\nsemantics in arbitrary ways:\n\n```clj\n(def fullstack-components\n  (-\u003e\u003e {:new-dependency [-\u003eNewDependency]}\n       (merge base-components)))\n\n;; =\u003e {:dependency     [-\u003eDependency arg1 arg2]\n;;     :dependent      [-\u003eDependent]\n;;     :new-dependency [-\u003eNewDependency]}\n\n(def fullstack-dependencies\n  (-\u003e\u003e {:dependent [:new-dependency]}\n       (run/merge-deps base-dependencies)))\n\n;; =\u003e {:dependent [:dependency :new-dependency]}\n\n(defn fullstack-system\n  []\n  (run/assemble-system fullstack-components fullstack-dependencies))\n```\n\nWhile a simple `merge` will work on the components maps, note the use\nof the `run/merge-deps` on the dependency maps.\n\n### Example System \u003ca name=\"example-system\"\u003e\u003c/a\u003e\n\nThere are a set of stub components assembled into an example system in\nthe\n[`runway.build`](https://github.com/zalky/runway/blob/main/build/runway/build.clj)\nnamespace. You can run this example system using:\n\n```sh\nclojure -X:server:dev\n```\n\nYou can try updating the components to see how the code and the\nrunning system are reloaded.\n\n### System Component Library \u003ca name=\"system-components\"\u003e\u003c/a\u003e\n\nYou can find a number of useful, ready made components (ex:\nwebsockets, servers, loggers, etc...) in the excellent\n[System](https://github.com/danielsz/system) library.\n\n## Main Invocation \u003ca name=\"main\"\u003e\u003c/a\u003e\n\nYou may want to configure your project with a `-main` function. Runway\nprovides CLI arg parsing for the `runway.core/go` method via\n`runway.core/cli-args` (implemented using `clojure.tools.cli`). For\nexample you could write something like:\n\n```clj\n(ns my.project\n  (:require [runway.core :as run]))\n\n(defn -main\n  [\u0026 args]\n  (let [{options :options\n         summary :summary\n         :as     parsed} (run/cli-args args)]\n    (if (:help options)\n      (println summary)\n      (do (run/go options)\n          @(promise)))))\n```\n\nHere, `args` are CLI args meant for `runway.core/go` (at minimum\n`--system my.project/app`), and not args to your running\napplication. To configure your actual application, prefer environment\nvariables, edn, or a configuration framework like Zookeeper.\n\n## Other Build Frameworks \u003ca name=\"other-framework\"\u003e\u003c/a\u003e\n\nThe default build implementation provided by Runway is for\n`com.stuartsierra.component/SystemMap`. However other build frameworks\ncan be wrapped to work with Runway. Simply wrap your system in a\nrecord that implements `com.stuartsierra.component/Lifecycle`, and\n`runway.core/IRecover`. You probably also want to set a custom\n`:restart-fn` watcher predicate. The default `:restart-fn` predicate\nrestarts your app whenever a direct dependent of\n`com.stuartsierra.component` changes, which is probably not what you\nwant for a non-Component build framework.\n\nSomething like this should work:\n\n```clj\n(ns my.project\n  (:require [com.stuartsierra.component :as component]\n            [runway.core :as run]))\n\n(defrecord ComponentWrapper [impl-system]\n  component/Lifecycle\n  (start [_]\n    (try\n      (-\u003eComponentWrapper (impl-start-fn impl-system))\n      (catch Throwable e\n        (let [id  (get-failed-id e)\n              sys (get-failed-system e)]\n          ;; Re-throw as component error\n          (throw\n           (ex-info (get-error-msg e)\n                    {:system-key id\n                     :system     (-\u003eComponentWrapper sys)\n                     :function   #'impl-start-fn}\n                    e))))))\n\n  (stop [_]\n    (try\n      (-\u003eComponentWrapper (impl-stop-fn impl-system))\n      (catch Throwable e\n        (let [id  (get-failed-id e)\n              sys (get-failed-system e)]\n          ;; Re-throw as component error\n          (throw\n           (ex-info (get-error-msg e)\n                    {:system-key id\n                     :system     (-\u003eComponentWrapper sys)\n                     :function   #'impl-stop-fn}\n                    e))))))\n\n  run/Recover\n  (recoverable-system [_ failed-id]\n    (-\u003e impl-system\n        (impl-recoverable-subsystem)\n        (-\u003eComponentWrapper))))\n\n(defn wrapped-app\n  \"Passed to runway.core/go\"\n  []\n  (-\u003eComponentWrapper (impl-system-constructor)))\n```\n\nThere are five important things to note:\n\n1. On error you need to re-throw a `com.stuartsierra.component`\n   compatible error. A component error is of type\n   `clojure.lang.ExceptionInfo` and contains at minimum:\n\n   - `:system-key`: This is the failed component id\n   - `:system`: This is the failed implementation system, wrapped in a\n   `ComponentWrapper`\n   - `:function`: This is the lifecycle that failed, only used for logging\n\n   Both the `:system-key` and the wrapped `:system` are passed to your\n   `recoverable-system` implementation, which needs to return a\n   wrapped subsystem that Runway will attempt to stop to recover from\n   the error. This subsystem should include everything that still\n   needs to be stopped _excluding_ the failed component.\n\n   If your `impl-start-fn` and `impl-stop-fn` do not throw errors, but\n   instead return errors as data, simply parse the data and then throw\n   a component exception.\n\n2. On success there's not much to do. Just return the started or\n   stopped implementation system, making sure it is wrapped.\n\n3. Wherever we delegate back to Runway, our implementation system must\n   always be wrapped in a `ComponentWrapper`. This includes the return\n   values of each protocol method, as well as in the re-thrown\n   component exception. It also includes the recoverable subsystem and\n   the constructor function passed to `runway.core/go`.\n\n4. If there's no need to recover your system on lifecycle errors, or\n   maybe `impl-start-fn` and `impl-stop-fn` handle recovery\n   directly, you can always choose not to re-throw errors in the\n   `start` and `stop` `Lifecycle` methods. In this case\n   `recoverable-system` is never called and `ComponentWrapper` becomes\n   trivial.\n\n5. Make sure you do not accidentally double wrap the component.\n\nSee\n[`runway.wrapped`](https://github.com/zalky/runway/blob/main/build/runway/wrapped.clj)\nfor a working stub that implements this full pattern where the other\n\"framework\" is just a simple Clojure map. You can run this wrapped\nexample system from the command line using:\n\n```clj\nclojure -X:server:dev '{runway.core/go {:system runway.wrapped/wrapped-app}}'\n```\n\n## License \u003ca name=\"license\"\u003e\u003c/a\u003e\n\nRunway is distributed under the terms of the Apache License 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalky%2Frunway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzalky%2Frunway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalky%2Frunway/lists"}