{"id":27230836,"url":"https://github.com/vodori/reactors","last_synced_at":"2025-08-01T05:04:57.636Z","repository":{"id":57729494,"uuid":"150913683","full_name":"vodori/reactors","owner":"vodori","description":"Maintain state, incorporate change, broadcast deltas. Reboot on error.","archived":false,"fork":false,"pushed_at":"2020-02-13T00:19:27.000Z","size":28,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":13,"default_branch":"develop","last_synced_at":"2023-08-12T18:22:11.587Z","etag":null,"topics":["agents","clojure","core-async","reactive","real-time"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vodori.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":"2018-09-30T00:23:27.000Z","updated_at":"2022-08-19T14:42:12.000Z","dependencies_parsed_at":"2022-09-11T06:07:23.640Z","dependency_job_id":null,"html_url":"https://github.com/vodori/reactors","commit_stats":null,"previous_names":[],"tags_count":1,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vodori%2Freactors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vodori%2Freactors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vodori%2Freactors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vodori%2Freactors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vodori","download_url":"https://codeload.github.com/vodori/reactors/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248225847,"owners_count":21068078,"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":["agents","clojure","core-async","reactive","real-time"],"created_at":"2025-04-10T13:33:52.125Z","updated_at":"2025-04-10T13:33:52.248Z","avatar_url":"https://github.com/vodori.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.com/vodori/reactors.svg?branch=develop)](https://travis-ci.com/vodori/reactors) [![Maven metadata URL](https://img.shields.io/maven-metadata/v/https/repo1.maven.org/maven2/com/vodori/reactors/maven-metadata.xml.svg)](https://mvnrepository.com/artifact/com.vodori/reactors)\n\n### Reactors\n\nA Clojure library to provide structure around shared in-memory state, incorporating \nlive changes, and broadcasting change deltas to observers. State is maintained using \nclojure agents and supports a configurable crash recovery strategy.\n\n### Rationale\n\nReactors provide leverage when implementing server-heavy collaborative processes \nby reigning in the independent sources of change and then broadcasting zero or more \nmessages to the subscribers based on differences between the old and new state.\n\nWhile you can certainly create lots of similar things yourself, \nreactors implements a couple of key ideas that we value:\n\n* A unified way to communicate state to new subscribers and existing subscribers as things change.\n* Recovery of errors that might have tainted the accumulated view of the current state. \n\n### Stability\n\nWe use reactors in a production capacity. We think the abstractions are simple \nbut useful and so are unlikely to change.\n\n\n___\n\n\n### Core Abstractions:\n\nA reactor consists of some source of initial state _(initializer)_, sources of \nchange _(publishers)_, observers of state _(subscribers)_, a function for \nincorporating change _(reducer)_, a function for describing changes in state \nto observers _(emitter)_, and functions that cleanup when a reactor implodes \n_(destroyers)_.\n\n\n#### Initializer\n\nA function of no arguments that is used to \"boot\" the reactor. Also used for any \"reboots\" \nafter a crash. It's just a function that should return the initial state to be contained in\nthe reactor before starting to incorporate events from publishers. This function is free to \ndo dangerous things. If anything fails it will be retried according to the recovery strategy.\n\n```clojure\n(defn initializer []\n  {})\n```\n\n\n#### Publishers\n\nA map of opaque identifiers to core.async channels representing sources of change. \nPublishers can be added to and removed from a reactor at any time.\n\n```clojure\n(def publishers \n {::database (get-database-change-stream-chan)\n  ::webhooks (get-incoming-webhook-events-chan)})\n```\n\n#### Subscribers\n\nA map of opaque identifiers to core.async channels representing observers of state. \nSubscribers can be added to and removed from a reactor at any time. \n\n```clojure\n(def subscribers \n {::paul (username-\u003ewebsocket-chan \"paul@example.com\")\n  ::eric (username-\u003ewebsocket-chan \"eric@example.com\")})\n```\n\n#### Reducer\n\nA function for incorporating change into current state. It's okay if this\nfunction does things like making database calls to gather additional information \nbecause it runs on the agent which can be restarted if it crashes. The first argument\nis the current state contained by the reactor and the second argument is a tuple of \nthe publisher identity and the event itself.\n\n```clojure\n(defn reducer [state [publisher event]]\n  (case (:kind event)\n    :insert (assoc state (:id event) (:data event))\n    :delete (dissoc state (:id event))\n    state))\n```\n\n#### Emitter\n\nA function for deciding what changes in state should be broadcast to subscribers. This\nfunction receives the old state and new state (after a successful run of the reducer)\nand should return a sequence of messages to be broadcast to subscribers. Your emitter must\nbe a pure function and should not make assumptions about the way in which state is known to\nchange in your application (it should detect them instead).\n\n```clojure \n(defn emitter [old-state new-state]\n  (let [[added removed] (data/diff (keys new-state) (keys old-state))]\n    (cond-\u003e []\n      (not-empty added) (conj {:event :added :data (mapv new-state added)})\n      (not-empty removed) (conj {:event :removed :data (mapv old-state removed)})))\n```\n\n#### Destructors\n\nFunctions of no arguments that perform housekeeping after a reactor implodes. Reactors\nimplode when they have exhausted the entire recovery strategy or when the reactor has \ngone from having some subscribers to having no subscribers.\n\n```clojure\n(def destructors\n  {::pool (fn [] (swap! reactor-pool disj reactor))})\n```\n\n___\n\n### Install\n\n``` \n[com.vodori/reactors \"0.1.0\"]\n```\n\n### Usage\n\n```clojure\n; wait 10 millis to start\n; double the wait every time\n; only restart up to 10 times (then implode!)\n(def recovery-policy\n  (take 10 (iterate (partial * 2) 10)))\n\n; create the reactor. This is a simplified example\n; but in reality, it's not unusual to close over\n; some unique per-reactor state inside the various \n; functions (reducer, emitter, initializer)\n(def reactor \n  (-\u003e {:reducer reducer \n       :emitter emitter \n       :backoff recovery-policy\n       :initializer initializer}\n       \n      (reactors/create-reactor)\n      \n      ; you can also do this any time throughout the\n      ; life of the reactor (as users come and go)\n      (reactors/add-publishers publishers)\n      (reactors/add-subscribers subscribers)\n      (reactors/add-destructors destructors)\n      \n      ; start listening to events from publishers\n      ; and broadcasting incorporated changes to any\n      ; subscribers\n      (reactors/start!)))\n\n\n; inside of your functions (reducer, emitter, initializer)\n; you can also access the 'current reactor'. Use the function\n; (reactors/current-reactor). It will return nil if not called\n; from within a reactor function.\n```\n\n___ \n\n### License\nThis project is licensed under [MIT license](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvodori%2Freactors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvodori%2Freactors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvodori%2Freactors/lists"}