-
-
Save randylien/46d1b9b6e75eb5618432 to your computer and use it in GitHub Desktop.
nucleus - a tiny flux-like architecture in clojurescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns a-nucleus-example | |
(:require-macros | |
[cljs.core.async.macros :refer [go go-loop]] | |
[nucleus.action :refer [defaction]]) | |
(:require | |
[nucleus.core :as nucleus :refer [dispatch! perform!]] | |
[nucleus.model :as model] | |
[om.core :as om] | |
[om-tools.core :refer-macros [defcomponent]] | |
[om-tools.dom :as dom :include-macros true] | |
;; hypothetical app | |
[app.api :as api] | |
[app.state.users :as users])) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Actions | |
;;; They do any needed async operations, then dispatch messages to models. | |
;;; Their inputs are current app state & any parameters | |
(defn fetch-user | |
"Fetches data for user if it's not already in state" | |
[app params] | |
(let [users (:users app) | |
user-id (:user-id params)] | |
(when-not (users/contains-user? users user-id) | |
(go | |
(when-let [response (<! (api/get-user user-id))] | |
(dispatch! :receive-user (:body response)))))) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; UI performs actions in response to interaction | |
(defcomponent fetch-user-button | |
"A button that fire-and-forgets the fetching of user's data" | |
[data owner] | |
(render [_] | |
(dom/button | |
{:on-click #(perform! fetch-user {:user-id (:user-id data)})} | |
"Fetch User Data"))) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Models | |
;;; They receive messages from actions | |
;;; They manage a single piece of state | |
;; A message-model receives messages of interested typespl | |
(def user-model | |
(model/message-model | |
{:receive-user (fn [users user] | |
(assoc users (:id user) user))})) | |
;; A dispatch-model composes other models to represent app state | |
(def model | |
(model/dispatch-model | |
{:users user-model})) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Rest of app | |
(def state (atom {})) | |
(nucleus/configure! state model) | |
(defcomponent app [data owner] | |
(render [_] | |
(dom/div | |
(om/build fetch-user-button {:user-id 123})))) | |
(om/root app state {:target js/document.body}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns nucleus.core | |
(:require-macros | |
[cljs.core.async.macros :refer [go go-loop]]) | |
(:require | |
[cljs.core.async :as async :refer [<! >!]] | |
[nucleus.message :as message] | |
[nucleus.model :as model])) | |
(def ^{:dynamic true :private true} *state* nil) | |
(def ^{:dynamic true :private true} *model* nil) | |
(defprotocol IDispatchable | |
(-dispatch! [message model state] | |
"Dispatches message to model given current state, returns next state.")) | |
(deftype Message [type params] | |
message/IMessage | |
(-message-type [_] type) | |
(-message-params [_] params) | |
IDispatchable | |
(-dispatch! [this model state] | |
(model/update-state model state this))) | |
(deftype MultiMessage [messages] | |
IDispatchable | |
(-dispatch! [_ model state] | |
(reduce | |
(fn [state message] | |
(-dispatch! message model state)) | |
state messages))) | |
(extend-protocol IDispatchable | |
Keyword | |
(-dispatch! [kw model state] | |
(-dispatch! (Message. kw nil) model state)) | |
nil | |
(-dispatch! [_ _ state] | |
state)) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Public | |
(defn message | |
"Returns a message to be sent to model via nucleus.model/update-state" | |
([type] | |
(message type nil)) | |
([type params] | |
(Message. type params))) | |
(defn multi-message | |
"Returns a message that wraps a set of messages, which will update the model | |
via nucleus.model/update-state. The messages are applied synchronously in order." | |
[& messages] | |
(MultiMessage. (flatten messages))) | |
(defn configure! | |
"Configures state and model for future actions. | |
Must be performed before any actions are dispatched." | |
[state model] | |
(set! *state* state) | |
(set! *model* model) | |
nil) | |
(defn dispatch! | |
"Sends message to model with current state and replaces current state if new value. | |
Returns nil." | |
([message-type params] | |
(dispatch! (message message-type params))) | |
([message] | |
(let [pstate @*state* | |
nstate (-dispatch! message *model* pstate)] | |
(when-not (= pstate nstate) | |
(reset! *state* nstate)) | |
nil))) | |
(defn perform! | |
"Performs action on the current state of the system. | |
Returns a channel that contains any errors and closes when action has completed." | |
([action] | |
(perform! action {})) | |
([action params] | |
(or (action @*state* params) | |
(doto (async/chan) (async/close!))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns nucleus.message | |
(:refer-clojure :exclude [type])) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Public | |
(defprotocol IMessage | |
(-message-type [this] | |
"Returns value identifying type of message") | |
(-message-params [this] | |
"Returns map of message's parameters if any, otherwise nil")) | |
(defn type [message] | |
(-message-type message)) | |
(defn params [message] | |
(-message-params message)) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Examples: Extend IMessage to work with preferred message format | |
(comment | |
;; Map style: {:type type, param-kvs...} | |
(extend-protocol IMessage | |
default | |
(-message-type [this] (:type this)) | |
(-message-params [this] (dissoc this :type))) | |
;; Tuple style: [type params] | |
(extend-protocol IMessage | |
default | |
(-message-type [this] (nth this 0)) | |
(-message-params [this] (nth this 1))) | |
;; Record style: #type { param-kvs ... } | |
(extend-protocol IMessage | |
default | |
(-message-type [this] (type this)) | |
(-message-params [this] this))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns nucleus.model | |
"Modeling and managing application state with pure functions" | |
(:require | |
[nucleus.message :as message])) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Public: Protocols | |
(defprotocol IModel | |
(init-state [this] | |
"Returns value the model is initialized to") | |
(update-state [this state message] | |
"Returns state, potentially updated from processing message")) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;; Public: Constructors | |
(defn message-model | |
"Returns a model, given a map of message-type to handler funciton. | |
Each handler function will be used to update state | |
Optionally accepts an initial state value or zero-arg function that returns | |
value of initial state." | |
([handler-map] (message-model handler-map nil)) | |
([handler-map init] | |
(reify | |
IModel | |
(init-state [_] init) | |
(update-state [_ state message] | |
(if-let [handler (get handler-map (message/type message))] | |
(handler state (message/params message)) | |
state))))) | |
(defn dispatch-model | |
"Returns a higher-order model that dispatches all update-state messages each model, | |
combining state into a single map. | |
Initial state is a map of each model in model-map." | |
[model-map] | |
(reify | |
IModel | |
(init-state [_] | |
(persistent! | |
(reduce-kv | |
(fn [t k model] (assoc! t k (init-state model))) | |
(transient {}) model-map))) | |
(update-state [_ state message] | |
(reduce-kv | |
(fn [s k model] | |
(let [model-state (if (contains? s k) (get s k) (init-state model)) | |
next-model-state (update-state model model-state message)] | |
;; Leave state untouched if message was no-op | |
(if-not (identical? model-state next-model-state) | |
(assoc s k next-model-state) | |
s))) | |
state | |
model-map)))) | |
(defn fn-model | |
"Returns a model that updates state by calling (f current-state message) | |
Optionally accepts an initial state value or zero-arg function that returns | |
value of initial state." | |
([f] (fn-model f nil)) | |
([f init] | |
(reify IModel | |
(init-state [_] init) | |
(update-state [_ state message] (f state message))))) | |
(def collector-model | |
"A model that collects all messages into a vector" | |
(fn-model conj [])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment