Skip to content

Instantly share code, notes, and snippets.

@randylien
Forked from loganlinn/a_nucleus_example.cljs
Last active September 21, 2015 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save randylien/46d1b9b6e75eb5618432 to your computer and use it in GitHub Desktop.
Save randylien/46d1b9b6e75eb5618432 to your computer and use it in GitHub Desktop.
nucleus - a tiny flux-like architecture in clojurescript
(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})
(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!)))))
(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)))
(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