├── system-structure3.jpg ├── shadow-cljs.edn ├── src ├── test │ └── fancoil │ │ └── test_core.cljs └── main │ └── fancoil │ ├── core.cljs │ ├── base.cljs │ ├── unit.cljs │ └── plugin.cljs ├── .gitignore ├── package.json ├── deps.edn ├── project.clj ├── LICENSE ├── pom.xml └── README.md /system-structure3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itarck/fancoil/HEAD/system-structure3.jpg -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:deps true 3 | 4 | :builds {:code {:target :npm-module 5 | :output-dir "public/js" 6 | :entries [fancoil.core]}}} 7 | -------------------------------------------------------------------------------- /src/test/fancoil/test_core.cljs: -------------------------------------------------------------------------------- 1 | (ns fancoil.test-core 2 | (:require 3 | [fancoil.core :as fc] 4 | [integrant.core :as ig])) 5 | 6 | 7 | (def config 8 | {::fc/ratom {}}) 9 | 10 | 11 | (ig/init config) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | *.calva-repl 3 | *.calva/ 4 | output.calva-repl 5 | *.lock 6 | .clj-kondo 7 | .DS_Store 8 | .lsp 9 | node_modules 10 | .shadow-cljs 11 | public/js 12 | target 13 | .lein-repl-history 14 | .vscode -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fancoil", 3 | "version": "0.0.7-SNAPSHOT", 4 | "private": true, 5 | "devDependencies": { 6 | "shadow-cljs": "2.16.8" 7 | }, 8 | "dependencies": { 9 | "react": "^17.0.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {thheller/shadow-cljs {:mvn/version "2.16.8"} 3 | binaryage/devtools {:mvn/version "1.0.3"} 4 | nrepl/nrepl {:mvn/version "0.8.3"} 5 | medley/medley {:mvn/version "1.3.0"} 6 | integrant/integrant {:mvn/version "0.8.0"} 7 | reagent/reagent {:mvn/version "1.1.0"}} 8 | 9 | :paths ["src/main" "src/test"]} 10 | 11 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.github.itarck/fancoil "0.0.7-SNAPSHOT" 2 | :description "A clojurescript framework, which uses multi-methods to define and implement system unit, uses integrant to inject configuration and stateful dependencies to unit at system startup." 3 | :url "https://github.com/itarck/fancoil" 4 | :license {:name "MIT License" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :source-paths ["src/main"] 7 | :dependencies [[org.clojure/core.async "1.5.648"] 8 | [integrant/integrant "0.8.0"] 9 | [reagent/reagent "1.1.0"] 10 | [medley/medley "1.3.0"]]) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 itarck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/fancoil/core.cljs: -------------------------------------------------------------------------------- 1 | (ns fancoil.core 2 | (:require 3 | [integrant.core :as ig] 4 | [fancoil.unit :as fu] 5 | [fancoil.plugin])) 6 | 7 | 8 | ;; helper functions 9 | 10 | (defn load-hierarchy 11 | [hierarchy] 12 | (doseq [[tag parents] hierarchy 13 | parent parents] 14 | (derive tag parent))) 15 | 16 | (defn merge-config 17 | [old-config new-config] 18 | (merge-with merge old-config new-config)) 19 | 20 | 21 | (def default-config 22 | {::fu/ratom {} 23 | ::fu/tap {} 24 | ::fu/inject {:ratom (ig/ref ::fu/ratom)} 25 | ::fu/do! {:ratom (ig/ref ::fu/ratom) 26 | :dispatch (ig/ref ::fu/dispatch)} 27 | ::fu/handle {:tap (ig/ref ::fu/tap)} 28 | ::fu/process {:ratom (ig/ref ::fu/ratom) 29 | :handle (ig/ref ::fu/handle) 30 | :inject (ig/ref ::fu/inject) 31 | :do! (ig/ref ::fu/do!)} 32 | ::fu/subscribe {:ratom (ig/ref ::fu/ratom)} 33 | ::fu/view {:dispatch (ig/ref ::fu/dispatch) 34 | :subscribe (ig/ref ::fu/subscribe)} 35 | ::fu/chan {} 36 | ::fu/dispatch {:out-chan (ig/ref ::fu/chan)} 37 | ::fu/service {:process (ig/ref ::fu/process) 38 | :in-chan (ig/ref ::fu/chan)} 39 | ::fu/schedule {:dispatch (ig/ref ::fu/dispatch)}}) 40 | 41 | -------------------------------------------------------------------------------- /src/main/fancoil/base.cljs: -------------------------------------------------------------------------------- 1 | (ns fancoil.base) 2 | 3 | 4 | (defmulti tap 5 | "pure function: tap a model 6 | value in -> value out 7 | " 8 | (fn [core method & args] method)) 9 | 10 | (defmulti handle 11 | "pure function: handle a request 12 | request in -> response out 13 | {:db db} -> {:tx tx}" 14 | (fn [core method & args] method)) 15 | 16 | (defmulti inject 17 | "stateful function: inject a cofx 18 | request in -> request out 19 | core: db-ref, other resources 20 | " 21 | (fn [core method & args] method)) 22 | 23 | (defmulti do! 24 | "stateful function: do a fx 25 | response in -> do effects 26 | core: db-ref, other resources 27 | " 28 | (fn [core method & args] method)) 29 | 30 | (defmulti process 31 | "stateful function: process a request to fx 32 | request in -> effects 33 | core: ratom, other resources 34 | " 35 | (fn [core method & args] method)) 36 | 37 | (defmulti subscribe 38 | "stateful function: subscribe a ratom or reaction 39 | reaction or ratom in -> reaction out 40 | core: db-ref 41 | " 42 | (fn [core method & args] method)) 43 | 44 | (defmulti view 45 | "stateful function: view a entity 46 | props in -> reagent views 47 | core: subscribe, dispatch 48 | " 49 | (fn [core method & args] method)) 50 | 51 | (defmulti schedule 52 | "stateful function: schedule a task once or periodic 53 | task in -> event out 54 | core: dispatch 55 | " 56 | (fn [core method & args] method)) 57 | 58 | -------------------------------------------------------------------------------- /src/main/fancoil/unit.cljs: -------------------------------------------------------------------------------- 1 | (ns fancoil.unit 2 | (:require 3 | [cljs.core.async :refer [go go-loop >! ! out-chan request))) 56 | ([method event] 57 | (dispatch method event {:sync? false})) 58 | ([method event args] 59 | (let [req (-> {:request/method method 60 | :request/body event} 61 | ((fn [event] 62 | (if (:sync? args) 63 | (assoc event :request/sync? true) 64 | event))))] 65 | (go (>! out-chan req)))))) 66 | 67 | (defmethod ig/init-key ::service 68 | [_ {:keys [process in-chan]}] 69 | (go-loop [] 70 | (let [request ( 2 | 4.0.0 3 | com.github.itarck 4 | fancoil 5 | jar 6 | 0.0.7-SNAPSHOT 7 | fancoil 8 | A clojurescript framework, which uses multi-methods to define and implement system unit, uses integrant to inject configuration and stateful dependencies to unit at system startup. 9 | https://github.com/itarck/fancoil 10 | 11 | 12 | MIT License 13 | https://opensource.org/licenses/MIT 14 | 15 | 16 | 17 | https://github.com/itarck/fancoil 18 | scm:git:git://github.com/itarck/fancoil.git 19 | scm:git:ssh://git@github.com/itarck/fancoil.git 20 | 367924d7a12bc34ad21922612d72fe1bb415756d 21 | 22 | 23 | src/main 24 | test 25 | 26 | 27 | resources 28 | 29 | 30 | 31 | 32 | resources 33 | 34 | 35 | target 36 | target/classes 37 | 38 | 39 | 40 | 41 | central 42 | https://repo1.maven.org/maven2/ 43 | 44 | false 45 | 46 | 47 | true 48 | 49 | 50 | 51 | clojars 52 | https://repo.clojars.org/ 53 | 54 | true 55 | 56 | 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.clojure 67 | core.async 68 | 1.5.648 69 | 70 | 71 | integrant 72 | integrant 73 | 0.8.0 74 | 75 | 76 | reagent 77 | reagent 78 | 1.1.0 79 | 80 | 81 | medley 82 | medley 83 | 1.3.0 84 | 85 | 86 | 87 | 88 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fancoil 2 | 3 | > What's the frontend of duct? 4 | > 5 | > You say air conditioning? Oh! Fan Coil Unit. 6 | 7 | 8 | A clojurescript framework, which uses [multi-methods] to define and implement system unit, uses [integrant] to inject configuration and stateful dependencies to unit at system startup. 9 | 10 | It is highly inspired by the structure of [re-frame] and [duct]. 11 | 12 | [integrant]:https://github.com/weavejester/integrant 13 | [multi-methods]:https://clojure.org/about/runtime_polymorphism 14 | [duct]:https://github.com/duct-framework/duct 15 | 16 | ## Installation 17 | 18 | [com.github.itarck/fancoil "0.0.7-SNAPSHOT"] ; Leiningen/Boot 19 | com.github.itarck/fancoil {:mvn/version "0.0.7-SNAPSHOT"} ; Clojure CLI/deps.edn 20 | 21 | ## How to use 22 | 23 | - Read the source code: not much, ~100 loc 24 | - See [fancoil.module], if you need some module to extend 25 | - fancoil.module.datascript:[datascript], an alternative to ratom 26 | - fancoil.module.posh:poshed datascript,you can use [posh] in subscribe 27 | - fancoil.module.cljs-ajax: a wrap for [cljs-ajax], as plugin of fancoil unit do! 28 | - fancoil.module.reitit.html-router: a [reitit] frontend router, already integranted with [accountant] and [clerk], originally from [reagent-template] 29 | - Try some examples in [fancoil-example] repo: includes simple clock, todomvc-ratom, todomvc-datascript, cat chat (with backend via http and ws) 30 | - [simple]: basic example 31 | - [simple-html-router]: use fancoil.module.reitit/html-router 32 | - [todomvc-ratom]: basic todomvc example, use ratom 33 | - [todomvc-posh]: use fancoil.module.posh 34 | - [catchat-full]: use fancoil.module.datascript, fancoil.module.cljs-ajax 35 | - [realworld-demo]: use fancoil.module.cljs-ajax, fancoil.module.reitit/html-router 36 | 37 | 38 | 39 | [fancoil-example]:https://github.com/itarck/fancoil-example 40 | [simple-html-router]:https://github.com/itarck/fancoil-example/tree/main/simple_html_router 41 | [todomvc-posh]:https://github.com/itarck/fancoil-example/tree/main/todomvc-posh 42 | [catchat-full]:https://github.com/itarck/fancoil-example/tree/main/catchat-full 43 | [realworld-demo]:https://github.com/itarck/fancoil-example/tree/main/realworld 44 | [simple]:https://github.com/itarck/fancoil-example/tree/main/simple 45 | [todomvc-ratom]:https://github.com/itarck/fancoil-example/tree/main/todomvc-ratom 46 | [posh]:https://github.com/denistakeda/posh 47 | [datascript]:https://github.com/tonsky/datascript 48 | [cljs-ajax]:https://github.com/JulianBirch/cljs-ajax 49 | [reitit]:https://github.com/metosin/reitit 50 | [clerk]:https://github.com/PEZ/clerk 51 | [accountant]:https://github.com/venantius/accountant 52 | [reagent-template]:https://github.com/reagent-project/reagent-template 53 | [fancoil-example]:https://github.com/itarck/fancoil-example 54 | [fancoil.module]:https://github.com/itarck/fancoil.module 55 | 56 | ## System structure 57 | 58 | ![System structure](https://github.com/itarck/fancoil/blob/main/system-structure3.jpg) 59 | 60 | 61 | ## Concept 62 | 63 | - System 64 | - The system has several machines working together, and it is stateful. 65 | - The system needs to follow a certain order when starting the machines. 66 | - Machine (unit) 67 | - Machines have three period: definition, implementation and runtime. 68 | - When a machine is running, it depends on other machines, and it is stateful. 69 | - If a machine is not running, it has no state. It is formal function that implements runtime functionality. 70 | - Plugin 71 | - Plugin can extend functionality of a machine 72 | - Module 73 | - Module can extend the system. Module is a package of a new machine and plugins for its related machines 74 | 75 | ## Types of machine 76 | 77 | | Name | Homies? | Desc | Spec | Detail | 78 | |---|---|---|---| --- | 79 | | db | | stored state | ref | ratom,datascript | 80 | | chan || flow state | channel | core.async.chan | 81 | | subscribe |✅| subscribe reaction | ref -> reaction | tree of reactions | 82 | | view |✅| view model | model -> reactions -> react component | reagent, rum | 83 | | dispatch || dispatch event | event -> request | | 84 | | tap |✅| tap model | value->value | user-defined, for handle, pure function | 85 | | process || process request | request -> effect | default to db-handler | 86 | | - inject |✅| inject co-effect | request -> request | support for multiple co-fx | 87 | | - handle |✅| handle request | request -> response | db-handler, pure function | 88 | | - do! |✅| do! effect | response -> effect | support for multiple fx | 89 | | service || long-run for request | go-loop | support for sync and async | 90 | | schedule || once/periodic | | e.g. init process | | 91 | 92 | ## Design Pattern of "Homies" 93 | 94 | Multimethod + integrant seems to be a kind of design pattern, I call it "Homies". Most machines are generated in the this way. The core of a homies is a hashmap which can contain config data, internal atom, external dependencies etc. 95 | 96 | ## Life cycle of "Homies" 97 | 98 | * Definition period: in fancoil.base, the abstraction of a machine is defined by defmulti. 99 | ``` clojure 100 | (defmulti process 101 | "stateful function 102 | request in -> effects out 103 | core: inject, handle, do!" 104 | (fn [core method & args] method)) 105 | ``` 106 | * Implementation period: in fancoil.plugin, some methods of base are implemented, you can include them. Or you can use defmethod to implement them in your project. Method may call other methods of same multi-fn, as is common in handle and subscribe. 107 | ``` clojure 108 | (defmethod base/process :default 109 | [{:keys [do! handle inject] :as core} method req] 110 | (let [req (inject :ratom/db req) 111 | resp (handle method req)] 112 | (do! :do/effect resp))) 113 | ``` 114 | * Runtime period: in fancoil.unit, some integrant init-key method is implemented, and integrant will inject the configuration into the machine when it initializes the system 115 | 116 | ``` clojure 117 | (defmethod ig/init-key ::process 118 | [_ config] 119 | (partial base/process config)) 120 | 121 | (def config 122 | {::process {:handle (ig/ref ::handle) 123 | :inject (ig/ref ::inject) 124 | :do! (ig/ref ::do!)} 125 | ;; other config }) 126 | ``` 127 | 128 | ## Features 129 | - Separation of state: stateful dependencies are injected until last minute 130 | - More functions: more pure or formal functions 131 | - Highly configurable: flexibility to change the system structure via integrant config 132 | - Highly extensible: extend for existing machines via plugins. Or write new integrant unit. 133 | - Easy to test: use integrant to init parts of the system to do unit tests on the machine 134 | 135 | 136 | ## Credits and Thanks 137 | - [@richhickey]: multi-methods, a powerful runtime polymorphism 138 | - [@weavejester]: [integrant], an elegant approach to system integration 139 | - [@day8]: [re-frame], a clear frontend application architecture 140 | 141 | [@richhickey]:https://github.com/richhickey 142 | [@weavejester]:https://github.com/weavejester 143 | [@day8]:https://github.com/day8 144 | [re-frame]:https://github.com/day8/re-frame 145 | 146 | ## Other notes 147 | - Still in PRE-ALPHA, some API may change. 148 | - Request is hash-map, open. when injecting cofx, it will add namespaced key of the injector. 149 | - Response is hash-map, open. (do! :do/effect response) can execute effect in response, but no guarantee of order. If you need to guarantee the order, use a vector of key-value pairs, just like vector form of a hash-map. 150 | --------------------------------------------------------------------------------