Skip to content

Commit

Permalink
Reflex: A ClojureScript state propagation library.
Browse files Browse the repository at this point in the history
  • Loading branch information
lynaghk committed May 27, 2012
0 parents commit f337805
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
.lein*
lib
public/*.js
target/
26 changes: 26 additions & 0 deletions LICENSE
@@ -0,0 +1,26 @@
Copyright (c) 2012, Kevin Lynagh
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* The name Kevin Lynagh may not be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL KEVIN LYNAGH BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47 changes: 47 additions & 0 deletions README.markdown
@@ -0,0 +1,47 @@
Reflex
======

Reflex is a ClojureScript library for automatic state propagation.
Intrinsic application state lives in atoms, from which derived values can be computed using Reflex-provided "computed observables" (COs).
COs always reflect the latest state, can depend on multiple atoms, and are themselves watchable (i.e., you can chain them).
COs are lazily evaluated, computing state only when they are dereferenced---they will notify their watches when they become dirty, but will not pass any values.

```clojure
(def a (atom 1))
(def b (computed-observable (inc @a)))
@b ;;=> 2
(reset! a 2)
@b ;;=> 3
```

To use from ClojureScript add this to your `project.clj`:

[com.keminglabs/reflex "0.1.0"]


Implementation
==============

Reflex monitors dereferencing of ClojureScript atoms to determine the atoms that each computed observerable depends on.
When any of these atoms change, the dependent CO is marked as dirty.
COs are lazy; their values will be calculated only when the CO is dereferenced (and only then if it dirty; otherwise the cached value is used).

Dependencies are recalculated every time the CO function is run, which means that dependencies can vary.
E.g., if atom `a` is truthy, then computed observable

```clojure
(computed-observable (if @a @b @c))
```

will depend only on `a` and `b`.
If, later, `a` becomes falsey, then the CO would be updated to depend only on `a` and `c`.


Thanks
======

Big hat tip to Knockout.js folks.
Not only did they write great code, they also took the time to write illuminating technical prose!

+ http://knockoutjs.com/documentation/computedObservables.html
+ https://github.com/mbest/knockout-deferred-updates
16 changes: 16 additions & 0 deletions project.clj
@@ -0,0 +1,16 @@
(defproject com.keminglabs/reflex "0.1.0-SNAPSHOT"
:description "ClojureScript state propagation."
:license {:name "BSD" :url "http://www.opensource.org/licenses/BSD-3-Clause"}

:dependencies [[org.clojure/clojure "1.4.0"]]

:min-lein-version "2.0.0"

:plugins [[lein-cljsbuild "0.1.10"]]

:source-paths ["src/clj" "src/cljs"]

:cljsbuild {:builds {:test {:source-path "test/cljs"
:compiler {:output-to "out/test/integration.js"
:optimizations :simple
:pretty-print true}}}})
12 changes: 12 additions & 0 deletions src/clj/reflex/macros.clj
@@ -0,0 +1,12 @@
(ns reflex.macros)

(defmacro capture-derefed [& body]
`(do
(reflex.core/reset-deref-watcher!)
(let [res# (do ~@body)]
{:res res# :derefed (reflex.core/recently-derefed)})))

(defmacro computed-observable [& body]
`(let [co# (reflex.core/ComputedObservable. nil true (fn [] ~@body) (gensym "computed-observable") {} {})]
@co# ;;initial deref to setup watchers
co#))
73 changes: 73 additions & 0 deletions src/cljs/reflex/core.cljs
@@ -0,0 +1,73 @@
(ns reflex.core
(:use-macros [reflex.macros :only [capture-derefed]]))

(def !recently-derefed
(atom #{} :meta {:no-deref-monitor true}))

(defn notify-deref-watcher! [derefable]
(when-not (:no-deref-monitor (meta derefable))
(swap! !recently-derefed #(conj % derefable))))

(defn reset-deref-watcher! []
(reset! !recently-derefed #{}))

(defn recently-derefed []
@!recently-derefed)

;;Have atoms make a note when they're dereferenced.
(extend-type Atom
IDeref
(-deref [this]
(notify-deref-watcher! this)
(.-state this)))


(defprotocol IDisposable
(dispose! [this]))


;;Similar to Knockout.js's "computed observable".
(defrecord ComputedObservable [state dirty? f key parent-watchables watches]
IWatchable
(-notify-watches [this _ _]
(doseq [[key wf] watches]
(wf)))
(-add-watch [this key wf]
(set! (.-watches this) (assoc watches key wf)))
(-remove-watch [this key]
(set! (.-watches this) (dissoc watches key)))

IDeref
(-deref [this]
(notify-deref-watcher! this)
(if-not dirty?
(.-state this)
(let [{:keys [res derefed]} (capture-derefed (f))]
;;Update watches on parent atoms.
;;This is necessary on each deref because the CO fn is arbitrary and can reference different atoms.
;;E.g., (if @a (... @b) (... @c))
(doseq [w parent-watchables]
(remove-watch w key))

(set! (.-parent-watchables this) derefed)

(doseq [w derefed]
(add-watch w key (fn []
(set! (.-dirty? this) true)
;;Notify watches that this CO is dirty.
(-notify-watches this nil nil))))

;;Update state
(set! (.-state this) res)
(set! (.-dirty? this) false)

;;Return newly calculated state.
res)))

IDisposable
(dispose! [this]
(doseq [w parent-watchables]
(remove-watch w key))

;;is this necessary for GC?
(set! (.-watches this) nil)))
1 change: 1 addition & 0 deletions test/cljs/integration.cljs
@@ -0,0 +1 @@
(ns test.integration)

0 comments on commit f337805

Please sign in to comment.