{"id":20780685,"url":"https://github.com/aroemers/redelay","last_synced_at":"2025-04-05T15:06:02.053Z","repository":{"id":62432747,"uuid":"265206638","full_name":"aroemers/redelay","owner":"aroemers","description":"Clojure library for first class lifecycle-managed state.","archived":false,"fork":false,"pushed_at":"2025-01-16T20:52:49.000Z","size":80,"stargazers_count":61,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T14:07:18.498Z","etag":null,"topics":["clojure","component","delay","integrant","lifecycle","mount","mount-lite","state-management"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aroemers.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-19T09:41:29.000Z","updated_at":"2025-01-16T20:52:36.000Z","dependencies_parsed_at":"2023-11-06T12:48:23.517Z","dependency_job_id":"5651b32d-9e92-4eaa-b387-5e365779cae6","html_url":"https://github.com/aroemers/redelay","commit_stats":{"total_commits":37,"total_committers":3,"mean_commits":"12.333333333333334","dds":"0.32432432432432434","last_synced_commit":"d3a1854d53d53b52e9a072f63998c2b64e719119"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fredelay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fredelay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fredelay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fredelay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aroemers","download_url":"https://codeload.github.com/aroemers/redelay/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247353731,"owners_count":20925329,"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","delay","integrant","lifecycle","mount","mount-lite","state-management"],"created_at":"2024-11-17T13:38:51.707Z","updated_at":"2025-04-05T15:06:02.032Z","avatar_url":"https://github.com/aroemers.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Clojars Project](https://img.shields.io/clojars/v/functionalbytes/redelay.svg)](https://clojars.org/functionalbytes/redelay)\n[![cljdoc badge](https://cljdoc.org/badge/functionalbytes/redelay)](https://cljdoc.org/d/functionalbytes/redelay/CURRENT)\n[![Clojure CI](https://github.com/aroemers/redelay/workflows/Clojure%20CI/badge.svg?branch=master)](https://github.com/aroemers/redelay/actions?query=workflow%3A%22Clojure+CI%22)\n[![Clojars Project](https://img.shields.io/clojars/dt/functionalbytes/redelay?color=blue)](https://clojars.org/functionalbytes/redelay)\n[![Blogpost](https://img.shields.io/badge/blog-Introducing%20redelay-blue)](https://functionalbytes.nl/clojure/redelay/rmap/2020/06/26/redelay.html)\n\n# 🏖 redelay\n\nA Clojure library for **state lifecycle-management** using **tracked** and **resettable** delays, inspired by [mount-lite](https://github.com/aroemers/mount-lite), decomplected from any methodology.\n\n![Banner](banner.png)\n\n## Usage\n\nThis library allows you to easily start and stop the stateful parts of your application, such as database connections, web servers, schedulers and caches.\nBeing able to easily restart these in the right order makes REPL-driven development easier, faster and _more fun_.\nThis is also known as Stuart Sierra's [reloaded workflow](https://www.cognitect.com/blog/2013/06/04/clojure-workflow-reloaded).\n\nWith this library you create first class **State** objects.\nThink of them as Clojure's [delay](https://clojuredocs.org/clojure.core/delay), but **resettable** and **tracked**.\n\n### The basics\n\nThe API is very small.\nAll you need to require is this:\n\n```clj\n(require '[redelay.core :refer [state status stop]])\n```\n\nTo create a State object you use the `state` macro, or the `defstate` macro as you'll see later on.\nLet's create two states for our examples, the second one depending on the first one.\n\n```clj\n(def config\n  (state (println \"Loading config...\")\n         (edn/read-string (slurp \"config.edn\")))\n\n(def db\n  (state :start  ; \u003c-- optional in this position\n         (println \"Opening datasource...\")\n         (make-datasource (:jdbc-url @config))\n\n         :stop\n         (println \"Closing datasource...\")\n         (close-datasource this)))\n```\n\nLet's quickly inspect one of the states we have just created:\n\n```clj\nconfig\n;=\u003e #\u003cState@247136[user/state--312]: :unrealized\u003e\n\n(realized? config)\n;=\u003e false\n```\n\nThere are several things to note here.\n\n- The expressions inside `state` are **qualified by a keyword**, such as `:start` and `:stop`.\n- The first expression is considered to be the `:start` expression, if not qualified otherwise.\n- All **expressions** are **optional**.\n- An expression can consist of **multiple forms**, wrapped in an implicit `do`.\n- The `:stop` expression has access to a **`this` parameter**, bound to the state's value.\n- You can call `clojure.core/realized?` on a state, just like you can on a delay.\n\nNow let's start and use our states.\nJust like a delay, the first time a state is consulted by a `deref` (or `force`), it is realized.\nThis means that the `:start` expression is executed and its result is cached.\n\n```clj\n@db\nLoading config...\nOpening datasource...\n;=\u003e org.postgresql.ds.PGSimpleDataSource@267825\n\n@db\n;=\u003e org.postgresql.ds.PGSimpleDataSource@267825\n\n(realized? config)\n;=\u003e true\n```\n\nYou can see that the `:start` expressions are only executed once.\nSubsequent derefs return the cached value.\n\nA state implements Java's `Closeable`, so you _could_ call `.close` on it.\nThis will execute the `:stop` expression and clear its cache.\nNow the state is ready to be realized again.\nThis is where it differs from a standard delay.\n\nHowever, **redelay keeps track of which states are realized and thus active.**\nYou can see which states are active by calling `(status)`:\n\n```clj\n(status)\n;=\u003e (#\u003cState@247136[user/state--312]: {:jdbc-url \"jdbc:postgresql:...\"}\u003e\n;=\u003e  #\u003cState@329663[user/state--315]: org.postgresql.ds.PGSimpleDataSource@267825\u003e)\n```\n\nBecause the active states are tracked, you can easily stop them all by calling `(stop)`.\nAll the active states are stopped (i.e. closed) in the **reverse order of their realization**.\nSo while you can call `.close` on the individual states, oftentimes you don't need to.\n\n```clj\n(stop)\nClosing datasource...\n;=\u003e (#\u003cState@329663[user/state--315]: :unrealized\u003e\n;=\u003e  #\u003cState@247136[user/state--312]: :unrealized\u003e)\n```\n\nSo **no matter where your state lives**, you can reset it and start afresh.\nEven if you've lost the reference to it.\n\nOh, two more things.\nIf an active state's `:stop` expression has a bug or can't handle its value, you can always force it to close with `close!`.\nAnd if you want to inspect the value of a state, without starting it, you can use Clojure's `peek` on it.\n\n### Naming and defstate\n\nNext to the `:start` and `:stop` expressions, you can pass a `:name` to the `state` macro.\nThis makes recognizing the states easier.\nThe `:name` expression must be a symbol.\n\n```clj\n(def config (state (load-config) :name user/config))\n;=\u003e #'user/config\n\nconfig\n;=\u003e #\u003cState@19042[user/config]: :unrealized\u003e\n```\n\nIf you bind your state to a global var, it is common to have the name to be equal to the var it is bound to.\nTherefore the above can also be written as follows:\n\n```clj\n(defstate config (load-config))\n```\n\nUsers of [mount](https://github.com/tolitius/mount) or [mount-lite](https://github.com/aroemers/mount-lite) will recognize above syntax.\nTrying to redefine a `defstate` which is active (i.e. realized) is skipped and yields a warning.\n\n#### Some other details\n\nThe `defstate` macro supports an optional docstring and attributes map.\nIt also supports metadata on the name symbol.\nNote that this metadata is set on the var.\nIf you want **metadata** on the State object, you can use `:meta` expression inside `state`, or use Clojure's `alter-meta!` or `reset-meta!` on it.\nSo a full `defstate` could look like this:\n\n```clj\n(defstate ^:private my-state\n  \"My docstring here.\"\n  {:extra \"attributes\"}\n  :start (start-it)\n  :stop  (stop-it this)\n  :meta  {:score 42})\n```\n\nNext to metadata support, Clojure's `namespace` and `name` functions also work on states.\nThis may yield an easier to read status list for example:\n\n```clj\n(map name (status))\n;=\u003e (\"config\")\n```\n\n### Testing your application\n\nSince state in redelay is handled as first class objects, there are all kinds of testing strategies.\nIt all depends a bit on where you keep your State objects (discussed in next section).\n\nFor the examples above you can simply use plain old `with-redefs` or `binding` to your hearts content.\nThe `binding` is possible for states created with `defstate`, as those are declared as dynamic for you.\n\nWe can redefine \"production\" states to other states, or even to a plain `delay`.\nThere is **no need for a special API** to support testing.\nFor example:\n\n```clj\n(deftest test-in-memory\n  (binding [config (delay {:jdbc-url \"jdbc:derby:...\"})]\n    (is (instance? org.apache.derby.jdbc.ClientDataSource @db))))\n```\n\nAnother option is to use Clojure's `with-open`, since states implement `Closeable`:\n\n```clj\n(deftest test-in-memory\n  (with-open [db (state (make-datasource \"jdbc:derby:...\"})\n    (is (instance? org.apache.derby.jdbc.ClientDataSource @db))))\n```\n\nYou could add a fixture in your test suite, ensuring `(stop)` is always called after a test.\n\nAgain, these are just examples.\nYou may structure and use your states completely different, as you'll see next.\n\n### Global versus local state\n\nThe examples above have bound the states to global vars.\nThis is not required.\n**State objects can live anywhere** and can be passed around like any other object.\nA less global approach using a map of states for example - either dereferenced or not - is perfectly feasible as well.\n\nBy its first class and unassuming nature this library aims to support **the whole spectrum** of [mount](https://github.com/aroemers/mount-lite)-like global states to [Component](https://github.com/stuartsierra/component)-like system maps to [Integrant](https://github.com/weavejester/integrant)-like data-driven approaches.\nThis is also the reason that redelay **does not have some sort of \"start\" or \"init\" function**.\nYou can easily add this to your application yourself, if you cannot to rely on derefs alone.\n\nBy the way, if you prefer system maps, have a look at **the [rmap](https://github.com/aroemers/rmap) library**, as it combines well with redelay.\n\n### Extending redelay\n\nThe redelay library is **minimal on purpose**.\nIt's just the the State object and the two basic management functions `(status)` and `(stop)`.\nThose two functions are actually implemented using the library's extension point: **the watchpoint**.\n\nThe library has a public `watchpoint` var.\nYou can watch this var by using Clojure's `add-watch`.\nThe registered watch functions receive `:starting`, `:started`, `:stopping` or `:stopped` and the State object.\n\nTry the following example:\n\n```clj\n(add-watch redelay.core/watchpoint :my-logger prn)\n```\n\nYou can do all kinds of things with this watchpoint, such as logging or keeping track of states yourself.\nSo if you want to have more sophisticated stop logic with separate buckets/systems of states using their metadata for example?\nGo for it, be creative and use the library's building blocks to fit your perfect workflow!\n\n_That's it for simple lifecycle management around the stateful parts of your application. Have fun!_ 🚀\n\n## License\n\nCopyright © 2020-2024 Functional Bytes\n\nThis program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0.\n\nThis Source Code may also be made available under the following Secondary\nLicenses when the conditions for such availability set forth in the Eclipse\nPublic License, v. 2.0 are satisfied: GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or (at your\noption) any later version, with the GNU Classpath Exception which is available\nat https://www.gnu.org/software/classpath/license.html.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroemers%2Fredelay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faroemers%2Fredelay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroemers%2Fredelay/lists"}