{"id":28941392,"url":"https://github.com/metosin/signaali","last_synced_at":"2025-10-23T23:12:23.750Z","repository":{"id":270116997,"uuid":"900029660","full_name":"metosin/signaali","owner":"metosin","description":"A small, portable \u0026 flexible implementation of lazy signals","archived":false,"fork":false,"pushed_at":"2025-05-30T08:40:21.000Z","size":43,"stargazers_count":66,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-06-18T18:04:10.391Z","etag":null,"topics":["clojure-library","clojurescript-library","metosin-experimental","reactive","reactive-programming","signaali"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/metosin.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":"2024-12-07T17:11:27.000Z","updated_at":"2025-05-30T08:40:24.000Z","dependencies_parsed_at":"2024-12-28T15:27:53.483Z","dependency_job_id":"0cacbc68-9159-405c-b8cb-6f9862a79935","html_url":"https://github.com/metosin/signaali","commit_stats":null,"previous_names":["metosin/signaali"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/metosin/signaali","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fsignaali","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fsignaali/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fsignaali/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fsignaali/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metosin","download_url":"https://codeload.github.com/metosin/signaali/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metosin%2Fsignaali/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261397490,"owners_count":23152492,"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-library","clojurescript-library","metosin-experimental","reactive","reactive-programming","signaali"],"created_at":"2025-06-23T02:10:27.724Z","updated_at":"2025-10-23T23:12:23.743Z","avatar_url":"https://github.com/metosin.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Signaali\n\n\u003e Naali: Finnish word for the [arctic fox](https://en.wikipedia.org/wiki/Arctic_fox).\n\n\u003e Sig: The name of an arctic fox.\n\n\u003e Signaali: Finnish word for signal.\n\n## Project status\n\n[![Clojars Project](https://img.shields.io/clojars/v/fi.metosin/signaali.svg)](https://clojars.org/fi.metosin/signaali)\n[![Slack](https://img.shields.io/badge/slack-signaali-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=signaali)\n[![cljdoc badge](https://cljdoc.org/badge/fi.metosin/signaali)](https://cljdoc.org/d/fi.metosin/signaali)\n\nSignaali is currently [experimental](https://github.com/metosin/open-source/blob/main/project-status.md#experimental).\nIt works and currently has no known bugs (if you find some, please file an issue),\nbut we might change its API or namespaces to make it reach maturity.\n\n- [Intro](#intro)\n- [Rationale](#rationale)\n- [Usage](#usage)\n- [How it works](#how-it-works)\n  - [Data phase, stale flagging propagation](#data-phase-stale-flagging-propagation)\n  - [Effect phase, pulling the new values \u0026 changing the world](#effect-phase-pulling-the-new-values--changing-the-world)\n- [State and memo nodes](#state-and-memo-nodes)\n- [Effect nodes](#effect-nodes)\n- [Execution ordering amongst the effects](#execution-ordering-amongst-the-effects)\n- [Disposing the nodes](#disposing-the-nodes)\n- [Unit testing](#unit-testing)\n- [Similar Clojure libraries](#similar-clojure-libraries)\n- [Projects using Signaali](#projects-using-signaali)\n- [AI context](doc/ai-context.md)\n- [License](#license)\n\n## Intro\n\nThis library contains a CLJC implementation of signals, which are used for building **reactive systems**.\nThe author is using it for building a web framework, but it could also be used for many other types of applications.\nYou can read more about signals [in this article](https://dev.to/milomg/super-charging-fine-grained-reactive-performance-47ph).\n\nWith Signaali, you can dynamically create and maintain a directed acyclic graph\nwhere the nodes are either:\n- representing an input data or signal, e.g. `(create-signal value)`,\n- representing a derived data, e.g. `(create-derived run-fn)`,\n- representing a side effect to be executed, e.g. `(create-effect run-fn)`.\n\n[![Youtube Video](https://github.com/user-attachments/assets/dda60a98-a4ac-4344-8a01-d75594235eaa)](https://www.youtube.com/watch?v=MYE99r00e7M)\n\n## Rationale\n\nThe code base was originally developed for an experimental front-end rendering library which needed a reactive system:\n- with strictly no glitches,\n- simple to reason about,\n- with a simple and small codebase.\n\nA few similar libraries existed already, but none satisfied the above criteria, so this library was created.\n\nThe code doesn't try to be \"the most performant\", as in many cases it is performant enough.\nInstead, a higher priority was placed on the developer convenience and simplicity.\nIf performance becomes a real need (e.g. better use of the memory and CPU), this library should be forked and tweaked - maybe some features\nwon't be needed in your specific use case.\n\nIf your use case is not covered by Signaali, let's have a talk on Slack and see if we can help.\n\n## Usage\n\n```clojure\n(require '[signaali.reactive :as sr])\n\n\n;; Data and derived data\n\n(def name-of-something (sr/create-signal \"Sig the arctic fox\"))\n@name-of-something ;; =\u003e \"Sig the arctic fox\"\n\n(def greeting-message (sr/create-derived (fn [] (str \"Hello, \" @name-of-something \"!\"))))\n@greeting-message ;; =\u003e \"Hello, Sig the arctic fox!\"\n\n(reset! name-of-something \"Sig naali\")\n@greeting-message ;; =\u003e \"Hello, Sig naali!\"\n\n\n;; Effects\n\n(def my-side-effect (sr/create-effect (fn [] (prn @greeting-message))))\n\n;; You can run the effect by hand:\n@my-side-effect  ;; \"Hello, Sig naali!\" is printed\n\n;; alternatively, you can enlist it as a stale effectful node for it\n;; to be run later, on the next call of `sr/re-run-stale-effectful-nodes`\n#_(sr/enlist-stale-effectful-node my-side-effect)\n\n(reset! name-of-something \"Alice\")\n;; Nothing is printed\n\n(reset! name-of-something \"Bob\")\n;; Nothing is printed\n\n(sr/re-run-stale-effectful-nodes)\n;; \"Hello, Bob!\" is printed\n\n(sr/re-run-stale-effectful-nodes)\n;; Nothing is printed\n\n\n;; Clean up\n\n(sr/dispose my-side-effect)\n\n(reset! name-of-something \"Coco\")\n\n(sr/re-run-stale-effectful-nodes)\n;; Nothing is printed\n```\n\n## How it works\n\nThe evaluation of the derived computations and the effects is done lazily, ensuring that each derived\ncomputation and effect that needs to be executed will be executed only once.\n\nThis is achieved by having 2 distinct phases:\n- When you modify the input data, the *\"data phase\"*\n- When you want the affected effects to re-run, the *\"effect phase\"*\n\n### Data phase, stale flagging propagation\n\nA signal can be modified similarly to a `clojure.core/atom` via `reset!` or `swap!`.\nWhen it happens, its signal watchers are notified of a change.\n\nEach node has a `status` which can be either `:up-to-date`, `:stale` for sure, or `:maybe-stale`.\nWhen notified, if his status was `:up-to-date`, it is changed to either `:stale` or `:maybe-stale`.\nWhen a node becomes stale, it notifies its signal watchers\n... and so on recursively, until there are no signal watchers left to notify.\n\nThe stale effect nodes are added to a set to remember them in the next phase.\n\n### Effect phase, pulling the new values \u0026 changing the world\n\nWhen a node is deref'ed (via `clojure.core/deref`, or via its shortcut character `@`),\nit always returns its up-to-date value.\n- Signal nodes will return their value directly.\n- Derived computation nodes and effect nodes will re-run if they are stale,\n  their status will be marked as `:up-to-date`,\n  the value returned from their run function will be stored,\n  then they will return it.\n\nDuring the effect phase, you typically will deref the effects which were marked stale.\nAs a user of the library, you decide when to do it:\nafter every single change or after a batch of changes, depending on your use-case.\n\n## State and memo nodes\n\nThere are 2 other nodes:\n- `(create-state value)` is the same as a signal node but will only propagate a change when\n  updated with value different from the previous one.\n- `(create-memo run-fn)` is the same as a derived node but will only propagate a change when\n  the value returned by its run function is different from before.\n\nState and memo nodes are by default using the function `sr/not-identical?` when\nfiltering the propagation of a change, but this can be overridden using an option.\n\nFor example:\n```clojure\n(create-state value {:propagation-filter-fn not=})\n```\n\n## Effect nodes\n\nYou can register a clean-up callback on each type of node.\nIt is called exactly once before each re-run of the effect, and also when the node is disposed.\n\nFor example:\n```clojure\n(def book-name\n  (sr/create-state \"Alice in wonderland\"))\n\n(def book-reader\n  (sr/create-effect\n   (fn []\n     (let [book-name @book-name]\n       (prn (str \"borrow \" book-name \" from library\"))\n       (sr/on-clean-up (fn [] (prn (str \"return \" book-name \" to library\"))))\n\n       (prn (str \"read \" book-name))\n\n       ;; An effect can return a value\n       {:page-count 100}))))\n\n(sr/enlist-stale-effectful-node book-reader)\n\n(def total-page-count\n  (sr/create-state 0))\n\n(def page-count-aggregator\n  (sr/create-effect\n   (fn []\n     (swap! total-page-count + (:page-count @book-reader)))))\n\n(sr/enlist-stale-effectful-node page-count-aggregator)\n\n(sr/re-run-stale-effectful-nodes)\n;; \"borrow Alice in wonderland from library\" is printed\n;; \"read Alice in wonderland\" is printed\n\n@total-page-count ; =\u003e 100\n\n(reset! book-name \"Pepper \u0026 Carrot\")\n\n(sr/re-run-stale-effectful-nodes)\n;; \"return Alice in wonderland to library\" is printed\n;; \"borrow Pepper \u0026 Carrot from library\" is printed\n;; \"read Pepper \u0026 Carrot\" is printed\n@total-page-count ; =\u003e 200\n```\n\n## Execution ordering amongst the effects\n\nWe can ensure an execution order between effects if they need to be re-run within the same\ncall of `sr/re-run-stale-effectful-nodes` via `(sr/run-after second-effect first-effect)`.\n\n```clojure\n(require '[signaali.reactive :as sr])\n\n(def data1 (sr/create-signal :data1))\n(def data2 (sr/create-signal :data2))\n\n(def effect1 (sr/create-effect (fn [] (prn :effect1 @data1))))\n(def effect2 (sr/create-effect (fn [] (prn :effect2 @data2))))\n\n(sr/enlist-stale-effectful-node effect1)\n(sr/enlist-stale-effectful-node effect2)\n\n(sr/re-run-stale-effectful-nodes)\n;; Lines printed in arbitrary order:\n;; :effect2 :data2\n;; :effect1 :data1\n\n(sr/run-after effect2 effect1)\n\n(reset! data1 :data1)\n(reset! data2 :data2)\n\n(sr/re-run-stale-effectful-nodes)\n;; Lines printed in deterministic order:\n;; :effect1 :data1\n;; :effect2 :data2\n\n;; Running effect1 doesn't force effect2 to run\n(reset! data1 :data1)\n\n(sr/re-run-stale-effectful-nodes)\n;; :effect1 :data1\n\n;; and vice-versa\n(reset! data2 :data2)\n\n(sr/re-run-stale-effectful-nodes)\n;; :effect2 :data2\n```\n\n## Disposing the nodes\n\nOnce a node is no longer used, you can dispose it.\n\nExample:\n```clojure\n(sr/dispose my-effect)\n```\n\nDisposing a node:\n- run its `on-clean-up` callback if any is registered,\n- unsubscribes it from all its sources (its dependencies),\n- unlists it from the `sr/stale-effectful-nodes` set,\n- unregisters it from the node priority data structure.\n\nBy default, the nodes will be disposed once their signal watcher count reaches zero.\nIf needed, this behavior can be avoided by using the `:dispose-on-zero-signal-watchers` option.\n\nFor example:\n```clojure\n(sr/create-derived \n (fn [] ,,,)\n {:dispose-on-zero-signal-watchers false})\n```\n\n## Unit testing\n\nThe tests run in both Clojure \u0026 Clojurescript.\n\n```bash\nnpm install\n./bin/kaocha\n```\n\n## Similar Clojure libraries\n\n- [Reagent Atom](https://github.com/reagent-project/reagent/blob/master/src/reagent/ratom.cljs), CLJS only.\n- [Signals](https://github.com/kunstmusik/signals), CLJ only.\n- [Flex](https://github.com/lilactown/flex), CLJC.\n- [Matrix](https://github.com/kennytilton/matrix), CLJC.\n- [Javelin](https://github.com/hoplon/javelin), CLJC.\n\nQuite different but on the same topic:\n- [Missionary](https://github.com/leonoel/missionary)\n\nPlease make a PR if you think that a library is missing from the list.\n\n## Projects using Signaali so far\n\nLibraries:\n\n- [Siagent](https://github.com/metosin/siagent): A rewrite of a subset of Reagent's features, using Signaali for the reactivity.\n- [Si-frame](https://github.com/metosin/si-frame): A fork of Re-frame, with the reactivity provided by Signaali via Siagent.\n- [Vrac](https://github.com/green-coder/vrac): A web framework in Clojure, for Clojurists. This is where Signaali was born.\n\nApplications:\n\n- A React native app for a customer (UIX + Si-frame)\n\n## License\n\nThis project is distributed under the [Eclipse Public License v2.0](LICENSE).\n\nCopyright (c) Vincent Cantin and contributors.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetosin%2Fsignaali","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetosin%2Fsignaali","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetosin%2Fsignaali/lists"}