├── .gitignore ├── DESIGN.md ├── LICENSE ├── OM_INTEGRATION.md ├── README.md ├── TODO.md ├── circle.yml ├── dev └── derive │ ├── debug_level.cljs │ └── repl.cljs ├── phantom ├── repl.js └── unit-test.js ├── project.clj ├── resources ├── public │ ├── css │ │ └── style.css │ └── index.html ├── templates │ └── js │ │ └── function_prototype_polyfill.js └── test │ └── index.html ├── src └── derive │ ├── core.clj │ ├── core.cljs │ ├── examples.temp │ ├── om.temp │ └── simple.cljs ├── test ├── derive │ ├── runner.cljs │ └── simple_test.cljs ├── phantomjs-shims.js └── phantomjs.js └── todo.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | .repl 2 | .nrepl-port 3 | target 4 | out 5 | pom.xml 6 | pom.xml.asc 7 | *jar 8 | /lib/ 9 | /classes/ 10 | /target/ 11 | /checkouts/ 12 | /resources/public/js/ 13 | .lein-deps-sum 14 | .lein-repl-history 15 | .lein-plugins/ 16 | .lein-failures 17 | *~ 18 | /resources/public/js/derive.js 19 | /.repl/client.js 20 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | Discussion 2 | =========== 3 | 4 | Derive exports a dependency tracking protocol and calling convention 5 | enabling, a deriving function to capture dependencies and track 6 | changes against a specific subset of the underlying state captured 7 | during calls to an underlying store. Derive functions memoize their 8 | answers and only recompute results when changes to the database would 9 | impact the answers provided by the derivation function. 10 | 11 | Derive is currently targeted at React/Om and our experimental library 12 | NativeStore, although it is intended to be a more general design 13 | pattern. In particular, Datascript should easily be adapted to this 14 | framework. 15 | 16 | The other motivation for Derive is replacing the existing 'explicit 17 | dataflow' semantics encouraged by frameworks like Hoplon and Pedestal 18 | App and libraries like Reflex. 19 | 20 | Om 0.1.5 State Management 21 | 22 | - The domain model and the rendering model in practice aren't always easily co-embedded 23 | - The representation we render from isn't always what we want to directly side-effect 24 | - Constraining information flow to a tree is artificial 25 | - Passing an index like Datascript down the tree is tedious. 26 | - No built-in method to implement effective cursors for database values (yet) 27 | - Changes to giant state models can be hard to reason about in practice 28 | - Sometimes hard to know what is a "cursor" value and what is a standard structure 29 | 30 | Pedestal App (0.3.0): 31 | 32 | - Complicated dataflow model connected via path naming conventions 33 | - Difficulty in identifying over 6-7 different files where a particular dataflow is happening - hard to trace effects to their root causes. 34 | - Potential for deadlock with cyclic core.async router dependencies (deprecated version 0.3.0) 35 | - Limited control over prioritization / concurrency 36 | - Heavy-weight tree-difference model for incremental updates that generate alot of garbage for GC 37 | - New data 'feeds forward' unless explicitly inhibited (e.g. by making updates conditional on whether a target widget is active or not, adding state management complexity to every transform.) 38 | - Difference to giant state models can be hard to reason about in practice. 39 | - Overly complex stateful widget architecture 40 | 41 | Functional-Reactive Systems (e.g. Hoplon) 42 | 43 | - 44 | Concepts 45 | ======== 46 | 47 | Following are some of the working thoughts we are developing as part 48 | of the Derive effort. 49 | 50 | A user interface is a rendering of several elements of state: 51 | 52 | 1a. Domain model - An underlying domain model often synced with a 53 | remote service and ideally indexed for fast lookups. 54 | 55 | 1b. Temporary domain model - When dealing with remote sychronization, 56 | the UI often wants to speculatively render what the server will 57 | eventually return (e.g. a note you are editing but isn't saved or a 58 | temporary model of something you just sent to the server). 59 | 60 | 2. Durable interface state - A representation of the structure and 61 | current state of the UI, possibly stored durably in local storage or 62 | even on the server. (e.g. current tab selections, current search query) 63 | 64 | 3. Dynamic local state - This is state in the UI that is ephemeral and 65 | can be thrown away. We use this for form editing, animations, drag & drop, 66 | DOM naming). 67 | 68 | Some Clojurescript UI framework authors emphasize the idea of having 69 | all state live in a single atom for simplicity and to support 70 | checkpointing and backtracking. We believe this is theoretically 71 | interesting, but creates problem when building complex systems in 72 | practice. 73 | 74 | When you have a two way connection with external state, you are 75 | limited in how far you can go back in history before you end up with a 76 | UI state that is wrong (represents stale data). The value of this 77 | behavior is vastly overstated and we should not be restricted in our 78 | choice of architectures by trying to adhere to this model. 79 | 80 | In Om, the typical design pattern combines #1a/b and #2 into the 81 | cursor tree at the root. Local state is dealt with locally and 82 | global, static state (e.g. lookup tables, multi-lingual translations) 83 | can be passed in shared state. 84 | 85 | The biggest problem we've found is that the design of the rendering 86 | function depends on having the domain model transformed into a 87 | structure suitable for rendering, including scattering operations on 88 | updates to push the domain representation to all the locations it 89 | needs to be rendered. Cursors let you mix and match subtrees when 90 | creating children, but what if you have deep nesting where some 91 | library method happens to need some index that the intermediate 92 | functions needn't know about (e.g. an auto-complete field)? For 93 | shallow hierarchies this works fine, but as UI grows more complex the 94 | coupling of top level and lower level components becomes a non-trivial 95 | source of incidental complexity. 96 | 97 | Instead, we propose to separate the representation of UI state so that 98 | the immutable specification passed to a component is simply the 99 | minimal parameter set needed to parameterize the component. The 100 | content of the component's UI is pulled from a global database that is 101 | shared across the render tree. 102 | 103 | Of course polluting rendering methods with index lookups, or a SQL or 104 | datalog query is messy and we haven't answered the question of how we 105 | rerender only when something we depend on in the data store has 106 | changed. We add to the above concept a collection of "derivation 107 | functions" that use parameters and a database to transform the raw 108 | domain state into a data model more suitable for rendering. 109 | 110 | e.g. 111 | 112 | ``` 113 | Component: Note[ID,expanded?] 114 | Depends on: (note db id), (subject db (:subject note)) ... 115 | ``` 116 | 117 | The component Note takes an ID and a boolean indicating whether it 118 | renders one of two UI states. When run it calls the derivation 119 | function (note db id) which is an immutable answer given an immutable state of 120 | the DB. Since the database can change, we need some way to be 121 | notified to rerun when a change to the DB would change the answer 122 | provided by note. 123 | 124 | If we are able to abstract the notion of changes to a persistent data 125 | structure, e.g. note(db,id) -> (get-in db [:model :note id]) provides 126 | a clear dependency on any side effects at or below the [:model :note 127 | id] path in the tree. If we control the accessors and mutators of the 128 | DB, it is possible to create record of what parts of the DB a call 129 | depends on and to see if a side effect to the database would change 130 | the value returned by note(db,id) (i.e. if the path [:model :note id] 131 | is unchanged, then note(db,id) is unchanged.. David Dixon has written 132 | an extension to Datascript that does this for datalog queries (index 133 | dependencies) which turns an invalidation test into a set intersection 134 | operation. This is essentially smart memoization. 135 | 136 | A derivation may depend on a primitive store or other derivations. A 137 | derivation registers it's dependencies with the underlying database 138 | dependencies submitted to a hook on a dynamic variable and stores the 139 | database value, dependency set, and return value so for any future 140 | calls with an identical database value or a new database value that 141 | does not match the dependency set, it just returns the prior answer. 142 | 143 | For integration into component rendering, the render method of a 144 | component can capture dependencies using the same mechanism, then 145 | listen to a feed of changes to the database and trigger a re-render 146 | iff any change matches the aggregate dependencies. If so, it will 147 | rerun the render, calling the derived functions which will invalidate 148 | and call their subsidiary functions, some of which may not have been 149 | invalidated (e.g. accesses to static data in the database). 150 | 151 | If a component unregistered it's dependencies when unmounted, then 152 | only the currently rendered components listen for changes and we only 153 | call derivation functions when the database has changed which 154 | results in lazy realization of the derived structures. 155 | 156 | The other advantage of derived structures is that we can add prismatic 157 | types to the derivation system so during development we have both an 158 | exact specification of what each function returns and the ability to 159 | perform runtime validation. We can have domain-oriented derivations 160 | and rendering-specific derivations. We can go so far as to have a 161 | derive function for every component that generates the model we would 162 | traditionally pass into Om, but without our parents being required to 163 | know anything about it except their their share usage of this family 164 | of methods. 165 | 166 | For derivations that are expensive, we can pre-compute the answer, for 167 | example in a web worker, and background update the derivation cache so 168 | future rendering is faster. 169 | 170 | We can have stream derivation functions that render the latest result 171 | of a long-running process so long as the process implements the 172 | derivation protocol. 173 | 174 | A derivation method is a pure function of a database reference 175 | followed by zero or more, possibly complex, parameters. Here is an 176 | example based on a datascript DB value: 177 | 178 | ``` 179 | (defn-derived note [db note-id] 180 | (->> (d/q '[:find ?note :in $ ?id :where 181 | [?note :id ?id]] 182 | db note-id) 183 | ffirst 184 | (d/entity db) 185 | prepare-for-rendering)) 186 | ``` 187 | 188 | This function returns a note object, converts it to a map, and runs a 189 | transform function (not a derive function) which modifies its state. 190 | 191 | Under the hood, the derived function tracks the internal dependencies 192 | of each database read operation and associates them with the result 193 | returned by the body. On subsequent calls with equal parameters, the 194 | method uses the previously captured dependencies to determine whether 195 | more recent values of the database invalidate the prior result. If 196 | not, it returns the previously cached result. This assumes that the 197 | dependency test is much cheaper than the time/space cost of 198 | recomputing a result. 199 | 200 | Derived methods can be nested, allowing top level methods to merge 201 | the dependencies of all its children, assuming the children are also 202 | pure functions of their arguments, and only recomputing the call tree 203 | if some child requires an update and then, only updating the children 204 | that need to be updated very similar to a React rendering tree. 205 | 206 | ``` 207 | Show nesting example here 208 | ``` 209 | 210 | Each database call checks for an active dependency *tracker* 211 | implementing the IDependencyTracker protocol. The tracker maintains a 212 | backing cache such that subsequent calls to notes with a possibly 213 | updated databases requires only a dependencies-changed? test to 214 | determine whether the body needs to be recomputed, or a memoized 215 | result returned. 216 | 217 | The dependency tracker can be used by non-derived functions using the 218 | macro capture-dependencies. The resulting dependency set can be 219 | explicitly stored and a transaction watcher can extract the dependency 220 | set implied by the latest transaction to determine whether the result of 221 | the body would change if rerun. 222 | 223 | ``` 224 | Show Om ShouldComponentUpdate here 225 | ``` 226 | 227 | Design 228 | ====== 229 | 230 | There are three derivation protocols that work together: 231 | 232 | 1. Dependencies - A representation of the dependency of an output on a database state and input 233 | 2. Sources - Can return dependencies and validate whether they are satisfied by a change to the source 234 | 3. DependencyTracker - Implemented by derivation functions and components, an internal protocol for managing a set of dependencies and determining if an existing response to an input is still valid. 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Vital Reactor LLC (http://vitalreactor.com) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /OM_INTEGRATION.md: -------------------------------------------------------------------------------- 1 | Extending Om via Derive Components 2 | ================================== 3 | 4 | We leverage the excellent work on Om/React, but choose a set of 5 | different conventions and add some functionality to the default Om 6 | component. 7 | 8 | 1. The Derive Component 9 | 10 | A derive component depends on a shared database reference and one or 11 | more parameters generated by the parent. Rather than passing down all 12 | data in these parameters, the render methods uses derive methods of 13 | the database and parameters to lookup the data it needs to render (the 14 | database as a value is static - meaning the current value of the 15 | database). When the DB changes, the component is marked dirty via 16 | 'om/refresh!' and will be re-rendered on the next animation cycle, 17 | calling the invalidated derivation function in turn. 18 | 19 | We implement this via a RenderDerived function that components 20 | call from their default render method with a binding context that 21 | captures dynamic dependencies just like a derive method. 22 | 23 | 2. Pure UI or Persistent UI state. 24 | 25 | Local state is use for memoryless-UI interactions (tab states, form 26 | state, etc.a) 27 | 28 | If the UI is persistent the lifecycle methods ensure that a database 29 | model with default parameters exists and uses that to store state. 30 | 31 | 3. Actions 32 | 33 | We eschew core.async for reasons of latency and the general hair of 34 | setting up little go loops in all our components. Instead we use 35 | callbacks passed down via Om's shared state mechanism to communicate 36 | with parents. Lateral communication, if needed, requires a 37 | collaborating parent to route messages (e.g. drag and drop). We 38 | provide some tools for setting up this interaction in Om. 39 | 40 | 4. Organization 41 | 42 | Typically we generate a model file which contains any prismatic schema 43 | data about the database for sanity checking and documentation 44 | purposes. We also implement some core derive methods to compute 45 | common functions of the model and action methods to perform changes to 46 | the model, create new models, etc. 47 | 48 | A d-component is a file with the component creation function, the 49 | RenderDerived method implementation and any component-specific derive 50 | methods. Controll components will also implement a set of action 51 | handlers that call action methods on appropriate objects. 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Derive 2 | ====== 3 | 4 | Derive is a dependency tracking framework that supports practical 5 | [Reactive Programming](http://en.wikipedia.org/wiki/Reactive_programming) in 6 | ClojureScript. 7 | 8 | Derive functions behave just like a memoizing function with automatic, 9 | behind-the-scenes cache invalidation and upstream notification when 10 | dependencies change. Dependencies are captured during the dynamic 11 | extent of the function execution for each unique set of parameters. 12 | Dependencies are produced by other derive functions and from one or 13 | more stores that implement derive's dependency tracking protocol. 14 | 15 | The built-in SimpleStore (derive.simple) serves as a working example 16 | and [NativeStore](http://github.com/vitalreactor/nativestore) is the 17 | first complete store to participate in the tracking protocol and is 18 | focused on efficient indexing and manipulation of native objects. 19 | 20 | It should be possible to adapt Dave Dixon's work on Datascript to 21 | support the derive model. 22 | 23 | ![Derive Architecture](https://docs.google.com/drawings/d/1lfblr7F8co5pXOmaeZ50Q1iqnplnjU3ynM3KaRPOqls/pub?w=953&h=876) 24 | 25 | Benefits 26 | ======== 27 | 28 | - Localize the logic specific to deriving and computing "renderable 29 | models" to the components that will consume them (instead of in the 30 | parent functions or external 'tree construction' logic) 31 | - Derive functions replace the need to compute and manage 32 | 'intermediate state' between your server and your render functions 33 | - Automatically notify the UI when the latest data it depended on 34 | changed after side effects to the source data are committed to the 35 | data store. 36 | - Parents in a render tree need minimal information about the 37 | representation manipulated by the child. The data passed in 38 | is more like function call parameters than a state model. 39 | - Avoid explicit specification of dataflow; the function call context 40 | within the body of a render loop will capture all dependencies. 41 | - Derivation functions become the api to the database. 42 | - Support dynamic programming: only derive a model when the underlying data 43 | has changed since the last call. Calling derive functions multiple times 44 | is cheap, just like rendering a component in Om is cheap when nothing has 45 | changed. 46 | 47 | 48 | Installing 49 | ========== 50 | 51 | Add derive to your dependencies. 52 | 53 | ```clj 54 | :dependencies [[com.vitalreactor/derive "0.2.0"]] 55 | ``` 56 | 57 | Then require it inside your code 58 | 59 | ```clj 60 | (ns foo.bar 61 | (:require [derive.core :as d :include-macros true])) 62 | 63 | (defnd note [store id] 64 | (transform-note (store id))) 65 | 66 | (defnd 67 | 68 | ``` 69 | 70 | Integration with Om/React 71 | ========================= 72 | 73 | ![Om/React Architecture](https://docs.google.com/drawings/d/11iQQ2r6XMKZ03LkAcRmIjSCMMrllc77q_oiLYYt8nzg/pub?w=960&h=720) 74 | 75 | Testing 76 | ======== 77 | 78 | - Test: lein with-profile test test 79 | - TDD: lein with-profile test cljsbuild auto test 80 | 81 | 82 | TODO 83 | ===== 84 | 85 | - Proper tutorial leveraging SimpleStore 86 | - Support explicit invalidation, memoized data TTL or LRU policies on 87 | fixed-size DependencyTracker caches (to recover memory in 88 | long-running programs), and to make these policies configurable with 89 | metadata. 90 | - Utilize speculative parallelism to pre-populate caches. In a 91 | threaded platform this reduces to a 'touch' operation running on a 92 | background thread. In Clojurescript web workers will require a 93 | little more machinery to keep a background DB in sync with the main 94 | DB and forward pre-computed state to derive functions on the main thread. 95 | 96 | 97 | 98 | Acknowledgements 99 | ================ 100 | 101 | This library was written by Ian Eslick and Dan Jolicoeur and benefited 102 | from discussions with Ryan Medlin and Dom Kiva-Meyer. We pulled ideas 103 | from quite a few other systems, but most heavily from work on Pedestal 104 | by Cognitect / Brenton Ashworth and the various React Clojurescript 105 | libraries Om/Reagent/etc. 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Orchestra Workflow Goals 2 | ======================== 3 | 4 | 1) Live editing JS/CSS 5 | - e.g. widgets file for Amelia and us in dev mode 6 | - e.g. dev-noww full site editing of CSS/JS 7 | - Ability to connect a REPL to this environment 8 | 9 | 2) Headless repl environment with the full code 10 | - Connect from Emacs 11 | - Meta-. navigation 12 | - Developing new derive widgets 13 | - C-x C-e eval optional 14 | 15 | 16 | React Integration 17 | ================= 18 | 19 | In our usage case, a component file contains: 20 | 21 | - Input schema specification 22 | - Component definition 23 | - One or more auxiliary render methods 24 | - One or more derivation functions called by render functions 25 | - A set of exported actions (also documented by schemas) 26 | 27 | On client applications there can be latency induced by the use of 28 | setInterval and setTimeout that encouraged us to provide some 29 | utilities around a callback dictionary that passed down the render 30 | tree in shared out-of-band state along with the database value. 31 | 32 | Higher level components have more system awareness and are able to 33 | determine where and how to update system state by plugging into 34 | 35 | TODO Spike 1 - Semantics 36 | ================ 37 | x 1) Simplest possible native data store (heap indexed by :id) 38 | x 2) Simplest possible value / range queries (simple indexing) 39 | 3) Pipe all models from service layer to native store 40 | N/A 4) Build derive functions that always recompute; force om models to always re-render 41 | 5) Develop derive functions for current mobile timeline (simple functions only) 42 | 6) Try executing mobile timeline using only non-pedestal models on a branch 43 | - Gives us a performance baseline 44 | - Compare to an om update that never renders (just to get performance gain possibility) 45 | 46 | TODO Spike 2 - Dependencies 47 | =============== 48 | x 1) Add transaction notification to native store 49 | x 2) Derive functions as IFn objects that cache results given params 50 | x 3) Capture query dependencies up derive call stack 51 | x 4) Derive functions associate store, params, deps, and result 52 | x 5) Derive functions listen to store txns passed into it 53 | x 6) Invalidate cache as appropriate (use store dependency API) 54 | 7) Develop Om extension to only re-render if any dependencies were invalidated 55 | 56 | TODO Spike 3 - Performance 57 | =============== 58 | x 1) Native store indexing 59 | x 2) Think carefully about copying 60 | x 3) Develop conventions around manipulation of native objects 61 | 4) Think about lazy/eager policies for derive fns 62 | (e.g. simple derive functions could update cache from txn without query) 63 | 64 | 65 | Other Desired Features 66 | ================ 67 | x Inhibit side effects to heap objects outside transaction! 68 | x Secondary indexing allowing map/sort/filter (without copying?) 69 | x Simple schemas to identify relational links among objects 70 | x Create "Reference" values on import that, when derefed, return a copy of the target object 71 | Turn on 'always copy' for debugging purposes 72 | 73 | 74 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - lein with-profiles base,test trampoline cljsbuild test 4 | -------------------------------------------------------------------------------- /dev/derive/debug_level.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.debug-level 2 | (:require [derive.tools :as tools])) 3 | 4 | (defn set-level! 5 | ([] (set-level! 2)) 6 | ([lvl] (tools/set-debug-level! lvl))) 7 | -------------------------------------------------------------------------------- /dev/derive/repl.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.repl 2 | (:require [clojure.browser.repl :as repl])) 3 | 4 | (defn connect [] 5 | (.log js/console "(repl/connect)") 6 | (repl/connect "http://localhost:9000/repl")) 7 | -------------------------------------------------------------------------------- /phantom/repl.js: -------------------------------------------------------------------------------- 1 | if (phantom.args.length != 1) { 2 | console.log('Expected a target URL parameter.'); 3 | phantom.exit(1); 4 | } 5 | 6 | var page = require('webpage').create(); 7 | var url = phantom.args[0]; 8 | var page_opened = false; 9 | 10 | page.onConsoleMessage = function (message) { 11 | console.log("App console: " + message); 12 | }; 13 | 14 | console.log("Loading URL: " + url); 15 | 16 | page.open(url, function (status) { 17 | // FIXME: This is a horrible, ridiculous hack. For some reason, PhantomJS calls 18 | // page.open() twice, and if doesn't return immediately the second time, 19 | // things break. Reference: 20 | // http://code.google.com/p/phantomjs/issues/detail?id=353&q=wait&sort=-type 21 | if (page_opened) { 22 | return; 23 | } 24 | page_opened = true; 25 | 26 | if (status != "success") { 27 | console.log('Failed to open ' + url); 28 | phantom.exit(1); 29 | } 30 | 31 | console.log("Loaded successfully."); 32 | }); 33 | -------------------------------------------------------------------------------- /phantom/unit-test.js: -------------------------------------------------------------------------------- 1 | if (phantom.args.length != 1) { 2 | console.log('Expected a target URL parameter.'); 3 | phantom.exit(1); 4 | } 5 | 6 | var page = require('webpage').create(); 7 | var url = phantom.args[0]; 8 | 9 | page.onConsoleMessage = function (message) { 10 | console.log("Test console: " + message); 11 | }; 12 | 13 | console.log("Loading URL: " + url); 14 | 15 | page.open(url, function (status) { 16 | if (status != "success") { 17 | console.log('Failed to open ' + url); 18 | phantom.exit(1); 19 | } 20 | 21 | console.log("Running test."); 22 | 23 | var result = page.evaluate(function() { 24 | return example.test.run(); 25 | }); 26 | 27 | // NOTE: PhantomJS 1.4.0 has a bug that prevents the exit codes 28 | // below from being returned properly. :( 29 | // 30 | // http://code.google.com/p/phantomjs/issues/detail?id=294 31 | 32 | if (result != 0) { 33 | console.log("*** Test failed! ***"); 34 | phantom.exit(1); 35 | } 36 | 37 | console.log("Test succeeded."); 38 | phantom.exit(0); 39 | }); 40 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.vitalreactor/derive "0.2.1" 2 | :description "Clojurescript library to support efficient computation of up to date values derived from a mutable data source. Designed to integrate with functional UI frameworks like Om and with data stores like Datascript and NativeStore" 3 | :url "http://github.com/vitalreactor/derive" 4 | :license {:name "MIT License" 5 | :url "http://github.com/vitalreactor/derive/blob/master/LICENSE"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/clojurescript "1.7.228"] 8 | [prismatic/schema "1.0.0"]] 9 | :plugins [[lein-cljsbuild "1.1.2"]] 10 | :hooks [leiningen.cljsbuild] 11 | :cljsbuild {:builds 12 | [ {:id "test" 13 | :source-paths ["src" "test"] 14 | :compiler {:output-to "resources/test/js/testable.js" 15 | :output-dir "resources/test/js/out" 16 | :output-map "resources/test/js/testable.js.map" 17 | :parallel-build true 18 | :optimizations :whitespace 19 | :recompile-dependents false 20 | :pretty-print true}}] 21 | :test-commands {"all" ["phantomjs" "test/phantomjs.js" "resources/test/index.html"]}}) 22 | 23 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

'Derive' Playground

9 |

Check out the dev console.

10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/templates/js/function_prototype_polyfill.js: -------------------------------------------------------------------------------- 1 | var Ap = Array.prototype; 2 | var slice = Ap.slice; 3 | var Fp = Function.prototype; 4 | 5 | if (!Fp.bind) { 6 | // PhantomJS doesn't support Function.prototype.bind natively, so 7 | // polyfill it whenever this module is required. 8 | Fp.bind = function(context) { 9 | var func = this; 10 | var args = slice.call(arguments, 1); 11 | 12 | function bound() { 13 | var invokedAsConstructor = func.prototype && (this instanceof func); 14 | return func.apply( 15 | // Ignore the context parameter when invoking the bound function 16 | // as a constructor. Note that this includes not only constructor 17 | // invocations using the new keyword but also calls to base class 18 | // constructors such as BaseClass.call(this, ...) or super(...). 19 | !invokedAsConstructor && context || this, 20 | args.concat(slice.call(arguments)) 21 | ); 22 | } 23 | 24 | // The bound function must share the .prototype of the unbound 25 | // function so that any object created by one constructor will count 26 | // as an instance of both constructors. 27 | bound.prototype = func.prototype; 28 | 29 | return bound; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /resources/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Nativestore Test 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/derive/core.clj: -------------------------------------------------------------------------------- 1 | (ns derive.core) 2 | 3 | (defmacro with-tracked-dependencies 4 | "Evaluate the body, tracking dependencies, and call the handler 5 | with a map of {src dep} and result" 6 | [[handler & [shadow?]] & body] 7 | `(let [parent-shadow# derive.core/*shadow* 8 | handler# ~handler 9 | shadow?# ~shadow?] 10 | (binding [derive.core/*tracker* (if derive.core/*shadow* 11 | derive.core/*tracker* 12 | (derive.core/default-tracker)) 13 | derive.core/*shadow* (or derive.core/*shadow* shadow?#)] 14 | (let [result# (do ~@body)] 15 | (when-not parent-shadow# 16 | (let [dmap# (derive.core/dependencies derive.core/*tracker*)] 17 | (handler# result# dmap#))) 18 | result#)))) 19 | 20 | (defmacro on-changes 21 | "Useful in contexts like an om render loop where we simply 22 | want to refresh the UI when a change is detected. e.g. 23 | (render [_] 24 | (derive/on-changes #(om/refresh owner) 25 | (html " 26 | [[subscribe-fn update-fn] & body] 27 | `(let [subscribe-fn# ~subscribe-fn 28 | cb# ~update-fn] 29 | (with-tracked-dependencies 30 | [(fn [result# dependency-map#] 31 | (subscribe-fn# cb# dependency-map#) 32 | (doseq [[store# query-deps#] dependency-map#] 33 | (derive.core/subscribe! store# cb# query-deps#)))] 34 | ~@body))) 35 | 36 | (defmacro defnd 37 | "Create a Derive function which manages the derive lifecycle for a 38 | set of function results that call out to other derive functions or 39 | source (store / databases). Currently the first argument to a derive 40 | function must be a source" 41 | [fname args & body] 42 | ;; TODO: 43 | ;; (if (exists? ~fname) ) 44 | ;; handle exists means informing listeners then recreation method 45 | `(def ~fname 46 | (let [derive# (derive.core/create-derive-fn ~fname) 47 | dfn# (fn ~(symbol (str (name fname) "-method")) 48 | [self# ~@args] 49 | (assert (derive.core/legal-params? ~(vec args)) 50 | "Parameters must be valid clojure values") 51 | (let [params# ~(vec args)] 52 | (if-let [value# (derive.core/derive-value self# params#)] 53 | value# 54 | (with-tracked-dependencies 55 | [(derive.core/tracker-handler self# params#)] 56 | ~@body)))) 57 | lfn# (fn ~(symbol (str (name fname) "-listener")) 58 | [lstore# ldeps#] 59 | (derive.core/derive-listener derive# lstore# ldeps#))] 60 | (set! (.-dfn derive#) dfn#) 61 | (set! (.-lfn derive#) lfn#) 62 | derive#))) 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/derive/core.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.core 2 | (:require [clojure.set :as s]) 3 | (:refer-clojure :exclude [reset!])) 4 | 5 | 6 | ;; ============================ 7 | ;; Dependency Tracking 8 | ;; ============================ 9 | 10 | (def ^{:doc "Dependency tracker that is informed of encountered dependencies" 11 | :dynamic true} 12 | *tracker* nil) 13 | 14 | (def ^{:doc "Whether a dependency tracker should shadow lower level, used to 15 | implement stores" 16 | :dynamic true} 17 | *shadow* nil) 18 | 19 | (defprotocol IDependencySet 20 | "An immutable set of dependencies. Passed to dependency trackers 21 | during queries via record-dependency." 22 | (merge-deps [this deps] 23 | "Merge two dependencies") 24 | (match-deps [this set] 25 | "Match the current set to an incoming set - intersection semantics")) 26 | 27 | (defprotocol IDependencySource 28 | "This interface is implemented by databases and derive methods to allow callers to 29 | subscribe to subset of changes identified by the provided dependency set" 30 | (subscribe! [this listener] [this listener deps] 31 | "Call tracker method when deps match a change operation") 32 | (unsubscribe! [this listener] [this listener deps] 33 | "Call tracker method when deps match a change operation") 34 | (empty-deps [this])) 35 | 36 | (defprotocol IDependencyTracker 37 | "Implemented by function and component caches" 38 | (depends! [this store deps] 39 | "Dependency sources call this method if a tracker is bound in the current 40 | context with dependencies that are encountered during query processing.") 41 | (dependencies [this] 42 | "The current dependencies encountered by this tracker")) 43 | 44 | (defprotocol IDependencyCache 45 | "A utility API for tracking dependencies, allows us to provide more 46 | advanced options for assembling tracker policies" 47 | (reset! [this] "Clear cache") 48 | (get-value [this params] 49 | "Returns cached value if exists for params") 50 | (add-value! [this params value dependency-map] 51 | "Informs store that a particular params yeilds value given current store + deps") 52 | (rem-value! [this params]) 53 | (invalidate! [this store deps])) 54 | 55 | 56 | ;; 57 | ;; Default dependency set 58 | ;; 59 | 60 | (extend-protocol IDependencySet 61 | PersistentHashSet 62 | (merge-deps [this deps] 63 | (s/union this deps)) 64 | (match-deps [this deps] 65 | (or (nil? deps) (not (empty? (s/intersection this deps))))) 66 | 67 | PersistentTreeSet 68 | (merge-deps [this deps] 69 | (s/union this deps)) 70 | (match-deps [this deps] 71 | (or (nil? deps) (not (empty? (s/intersection this deps)))))) 72 | 73 | ;; 74 | ;; Simple tracker 75 | ;; 76 | 77 | (defn- matching-dep? [dmap store deps] 78 | (some (fn [[vstore vdeps]] 79 | #_(.log js/console "Matching: " 80 | (= store vstore) 81 | (pr-str deps) 82 | (pr-str vdeps) 83 | (match-deps vdeps deps) 84 | (and (= store vstore) 85 | (or (nil? deps) (nil? vdeps) 86 | (match-deps vdeps deps)))) 87 | (and (= store vstore) 88 | (or (nil? deps) (nil? vdeps) 89 | (match-deps vdeps deps)))) 90 | dmap)) 91 | 92 | (deftype DefaultCache [^:mutable cache] 93 | IDependencyCache 94 | (get-value [_ params] (get cache params)) 95 | (add-value! [this params value dmap] 96 | (set! cache (assoc cache params [value dmap])) 97 | this) 98 | (rem-value! [this params] 99 | (set! cache (dissoc cache params))) 100 | (invalidate! [this store deps] 101 | (let [[c invalidated] 102 | (reduce (fn [[c i] [params [value dmap]]] 103 | #_(.log js/console "Matching result: " (matching-dep? dmap store deps)) 104 | (if (matching-dep? dmap store deps) 105 | (do 106 | #_(.log js/console "Invalidating: " (pr-str params)) 107 | [(dissoc! c params) (conj i params)]) 108 | [c i])) 109 | [(transient cache) []] 110 | cache)] 111 | (set! cache (persistent! c)) 112 | invalidated)) 113 | (reset! [this] (set! cache {}) this)) 114 | 115 | (defn default-cache [] 116 | (DefaultCache. {})) 117 | 118 | (deftype DefaultTracker [^:mutable dmap] 119 | IDependencyTracker 120 | (depends! [this store new-deps] 121 | #_(.log js/console "depends!: " dmap (pr-str new-deps)) 122 | (set! dmap (update-in dmap [store] (fnil merge-deps (empty-deps store)) new-deps)) 123 | #_(.log js/console " " dmap) 124 | this) 125 | 126 | (dependencies [this] dmap)) 127 | 128 | (defn default-tracker 129 | ([] (DefaultTracker. {})) 130 | ([dmap] (DefaultTracker. dmap))) 131 | 132 | ;; Utilities for stores 133 | 134 | (defn tracking? [] (not (nil? *tracker*))) 135 | 136 | (defn inform-tracker 137 | ([store args] 138 | (when (tracking?) 139 | (inform-tracker *tracker* store args))) 140 | ([tracker store args] 141 | #_(.log js/console "Informing tracker: " args " t? " *tracker*) 142 | (depends! tracker store (if (set? args) args #{args})))) 143 | 144 | 145 | ;; 146 | ;; Derive Function 147 | ;; 148 | 149 | (deftype DeriveFn [fname dfn lfn ^:mutable subscriptions ^:mutable cache ^:mutable listeners] 150 | Fn 151 | IFn 152 | (-invoke [this] 153 | (dfn this)) 154 | (-invoke [this a] 155 | (inform-tracker this [a]) 156 | (dfn this a)) 157 | (-invoke [this a b] 158 | (inform-tracker this [a b]) 159 | (dfn this a b)) 160 | (-invoke [this a b c] 161 | (inform-tracker this [a b c]) 162 | (dfn this a b c)) 163 | (-invoke [this a b c d] 164 | (inform-tracker this [a b c d]) 165 | (dfn this a b c d)) 166 | (-invoke [this a b c d e] 167 | (inform-tracker this [a b c d e]) 168 | (dfn this a b c d e)) 169 | (-invoke [this a b c d e f] 170 | (inform-tracker this [a b c d e f]) 171 | (dfn this a b c d e f)) 172 | (-invoke [this a b c d e f g] 173 | (inform-tracker this [a b c d e f g]) 174 | (dfn this a b c d e f g)) 175 | (-invoke [this a b c d e f g h] 176 | (inform-tracker this [a b c d e f g h]) 177 | (dfn this a b c d e f g h)) 178 | (-invoke [this a b c d e f g h i] 179 | (inform-tracker this [a b c d e f g h i]) 180 | (dfn this a b c d e f g h i)) 181 | (-invoke [this a b c d e f g h i j] 182 | (inform-tracker this [a b c d e f g h i j]) 183 | (dfn this a b c d e f g h i j)) 184 | (-invoke [this a b c d e f g h i j k] 185 | (inform-tracker this [a b c d e f g h i j k]) 186 | (dfn this a b c d e f g h i j k)) 187 | (-invoke [this a b c d e f g h i j k l] 188 | (inform-tracker this [a b c d e f g h i j k l]) 189 | (dfn this a b c d e f g h i j k l)) 190 | (-invoke [this a b c d e f g h i j k l m] 191 | (inform-tracker this [a b c d e f g h i j k l m]) 192 | (dfn this a b c d e f g h i j k l m)) 193 | (-invoke [this a b c d e f g h i j k l m n] 194 | (inform-tracker this [a b c d e f g h i j k l m n]) 195 | (dfn this a b c d e f g h i j k l m n)) 196 | (-invoke [this a b c d e f g h i j k l m n o] 197 | (inform-tracker this [a b c d e f g h i j k l m n o]) 198 | (dfn this a b c d e f g h i j k l m n o)) 199 | (-invoke [this a b c d e f g h i j k l m n o p] 200 | (inform-tracker this [a b c d e f g h i j k l m n o p]) 201 | (dfn this a b c d e f g h i j k l m n o p)) 202 | (-invoke [this a b c d e f g h i j k l m n o p q] 203 | (inform-tracker this [a b c d e f g h i j k l m n o p q]) 204 | (dfn this a b c d e f g h i j k l m n o p q)) 205 | (-invoke [this a b c d e f g h i j k l m n o p q r] 206 | (inform-tracker this [a b c d e f g h i j k l m n o p q r]) 207 | (dfn this a b c d e f g h i j k l m n o p q r)) 208 | (-invoke [this a b c d e f g h i j k l m n o p q r s] 209 | (inform-tracker this [a b c d e f g h i j k l m n o p q r s]) 210 | (dfn this a b c d e f g h i j k l m n o p q r s)) 211 | (-invoke [this a b c d e f g h i j k l m n o p q r s t] 212 | (inform-tracker this [a b c d e f g h i j k l m n o p q r s t]) 213 | (dfn this a b c d e f g h i j k l m n o p q r s t)) 214 | (-invoke [this a b c d e f g h i j k l m n o p q r s t rest] 215 | (inform-tracker this [a b c d e f g h i j k l m n o p q r s t rest]) 216 | (apply dfn this a b c d e f g h i j k l m n o p q r s t rest)) 217 | 218 | IDependencySource 219 | (subscribe! [this listener] 220 | (set! listeners (update-in listeners [nil] (fnil conj #{}) listener))) 221 | (subscribe! [this listener deps] 222 | (set! listeners (update-in listeners [deps] (fnil conj #{}) listener))) 223 | (unsubscribe! [this listener] 224 | (set! listeners (update-in listeners [nil] disj listener))) 225 | (unsubscribe! [this listener deps] 226 | (set! listeners (update-in listeners [deps] disj listener))) 227 | (empty-deps [this] #{})) 228 | 229 | (defn legal-param? 230 | "Only primitive values and clojure data stuctures are legal 231 | (must support value equality)" 232 | [val] 233 | (or (string? val) 234 | (number? val) 235 | (symbol? val) 236 | (keyword? val) 237 | (= val true) 238 | (= val false) 239 | (map? val) 240 | (vector? val) 241 | (list? val) 242 | (nil? val) 243 | (satisfies? IDependencySource val) 244 | (= (type val) (type (js/Date.))) 245 | (and (not (undefined? js/moment)) 246 | (= (type val) (type (js/moment)))))) 247 | 248 | (defn legal-params? [vals] 249 | (every? legal-param? vals)) 250 | 251 | (defn empty-derive-fn [& args] 252 | (assert false "Uninitialized derive fn")) 253 | 254 | (defn create-derive-fn [fname] 255 | (DeriveFn. fname empty-derive-fn empty-derive-fn 256 | #{} (derive.core/default-cache) {})) 257 | 258 | (defn ensure-subscription 259 | "Ensure we're subscribed to stores we encounter" 260 | [derive store] 261 | (when-not ((.-subscriptions derive) store) 262 | (subscribe! store (.-lfn derive)) 263 | (set! (.-subscriptions derive) (conj (.-subscriptions derive) store)))) 264 | 265 | (defn release-subscriptions [derive] 266 | (doall (map #(unsubscribe! % (.-lfn derive)) (.-subscriptions derive)))) 267 | 268 | (defn derive-value 269 | "Handle deps and cache values from normal calls" 270 | [derive params] 271 | (first (get-value (.-cache derive) params))) 272 | 273 | (defn tracker-handler [dfn params] 274 | (fn [result dmap] 275 | #_(.log js/console "tracker handler: " result dmap) 276 | (doseq [[store deps] dmap] 277 | (ensure-subscription dfn store)) 278 | (add-value! (.-cache dfn) params result dmap))) 279 | 280 | (defn notify-listeners [store deps] 281 | (let [listeners (.-listeners store)] 282 | #_(.log js/console "Update listeners " (pr-str listeners) (pr-str deps)) 283 | (doseq [ldeps (keys listeners) :when (or (nil? ldeps) (match-deps ldeps deps))] 284 | (doseq [l (get listeners ldeps)] 285 | (l store deps))))) 286 | 287 | (defn force-invalidation 288 | "Force invalidate all listeners to this store" 289 | [store] 290 | (let [listeners (.-listeners store)] 291 | (doseq [ldeps (keys listeners)] 292 | (doseq [l (get listeners ldeps)] 293 | (l store nil))))) 294 | 295 | (defn derive-listener 296 | "Helper. Handle source listener events" 297 | [derive store deps] 298 | #_(.log js/console "Derive received: " (or (.-deps deps) deps)) 299 | (let [cache (.-cache derive) 300 | param-set (set (invalidate! cache store deps))] 301 | (when-not (empty? param-set) 302 | (notify-listeners derive param-set)))) 303 | 304 | (defn invalidate-all-listeners 305 | "Helper. Inform upstream when we're redefined" 306 | [derive] 307 | (doall 308 | (map (fn [f] (f derive nil)) 309 | (flatten (vals (.-listeners derive)))))) 310 | 311 | ;; 312 | ;; Om Support 313 | ;; 314 | 315 | (defn clear-listener! 316 | "Call from will-unmount and when re-subscribing a component" 317 | [owner] 318 | (let [listener (aget owner "__derive_listener") 319 | dmap (aget owner "__derive_dmap")] 320 | (doseq [[store query-deps] dmap] 321 | (derive.core/unsubscribe! store listener query-deps)) 322 | (aset owner "__derive_listener" nil) 323 | (aset owner "__derive_dmap" nil) 324 | owner)) 325 | 326 | (defn save-listener! [owner listener dmap] 327 | (aset owner "__derive_listener" listener) 328 | (aset owner "__derive_dmap" dmap) 329 | owner) 330 | 331 | (defn- om-subscribe-handler 332 | "Call in on-changes" 333 | [owner] 334 | (fn [listener dmap] 335 | #_(.log js/console "Got subscribe callback: " (pr-str dmap)) 336 | (-> owner 337 | (clear-listener!) 338 | (save-listener! listener dmap)))) 339 | 340 | -------------------------------------------------------------------------------- /src/derive/examples.temp: -------------------------------------------------------------------------------- 1 | (ns derive.examples 2 | (:require [derive.core :as core :import-macros true])) 3 | 4 | (comment 5 | 6 | (defn gen-schema [records] 7 | (into {} (map (fn [[attr & tags]] 8 | (let [tags (set tags)] 9 | [attr (cond-> {} 10 | (tags :many) 11 | (assoc :db/cardinality :db.cardinality/many) 12 | (tags :ref) 13 | (assoc :db/valueType :db.type/ref))])) 14 | records))) 15 | 16 | (def schema 17 | (gen-schema 18 | [[:member :ref :many] 19 | [:contains :ref :many "Resource Share, Account Group"] 20 | [:canRead :ref :many "Account Share resources"] 21 | [:canWrite :ref :many "Account Share resources"] 22 | [:canTrack :ref :many "Account Subject and will see them on mobile"] 23 | [:self :ref "Account Subject where Account == Subject"] 24 | ;; Subject 25 | [:subject/share :ref :many] 26 | ;; Measure 27 | [:channel :many] 28 | ;; Tracker 29 | [:account :ref] 30 | [:subject :ref] 31 | [:measure :ref] 32 | ;; Tasks 33 | [:parent :ref] 34 | [:reference :ref] 35 | ;; Notes 36 | [:reply-to :ref]])) 37 | 38 | 39 | ;; TODO: Inject Test Data 40 | 41 | (defn-derived notes-by-task 42 | [ds task-id] 43 | (d/q [:find ?n :in $ ?tid :where 44 | [?n :reference ?tid]] 45 | (:ds app) task-id)) 46 | 47 | (d/defn-derived notes-by-tracker 48 | [ds track-id] 49 | (d/q [:find ?n :in $ ?tid :where 50 | [?n :reference ?tid]] 51 | (:ds app) track-id)) 52 | 53 | (d/defn-derived observations 54 | [ds tracker-id] 55 | (d/query [:find ?obs :in $ ?tid :where 56 | [?tid :parent ?track] 57 | [?ts :type :time-series] 58 | [?ts :tracker ?track] 59 | [?ts :observations ?obs]])) 60 | 61 | (d/defn-derived annotated-observations 62 | [conn tracker-id] 63 | (let [obs (observations ds tracker-id) 64 | notes (notes-by-tracker ds tracker-id)] 65 | (group-by :reference notes))) 66 | 67 | (d/defn-derived card 68 | [conn task-id] 69 | {:pre [(not (nil? task-id))]} 70 | (let [db (:ds app) 71 | task (d/by-id (:ds app) task-id) 72 | tracker (d/entity db (:tracker task)) 73 | measure (d/entity db (:measure tracker)) 74 | notes ] 75 | {:track-id (:id tracker) 76 | :prompt (:prompt measure) 77 | :notes notes})) 78 | 79 | ;; TODO: Define make some calls 80 | ;; TODO: Demo memoization by inspection 81 | -------------------------------------------------------------------------------- /src/derive/om.temp: -------------------------------------------------------------------------------- 1 | (ns derive.om) 2 | 3 | (comment 4 | 5 | (defprotocol IRenderDerived [_ state]) 6 | 7 | (def DeriveMixin 8 | #js {:componentWillMount 9 | (fn [] 10 | ;; subscribe to transactions in shared :conn 11 | ;; setup initial dependencies on component (empty) 12 | ;; txn listener calls refresh! on owner 13 | ) 14 | :componentWillUnmount 15 | (fn [] 16 | ;; unsubscribe to transactions in shared :conn 17 | )}) 18 | 19 | ;; Use a ctor to override default render? 20 | (defn derive-ctor [] 21 | (should-component-update [_ state] 22 | (check m/annotated-observations cursor)) 23 | (render [_ state] 24 | (let [card (annotated-observations (omd/conn) task-id)] 25 | (render-card card)))) 26 | 27 | (omd/defcomponent 28 | (init-state [_]) 29 | (render-state [_ state]) 30 | (will-mount [_]) 31 | (did-mount [_])) 32 | 33 | ;(updated? annotated-observations app task-id) 34 | ;(updated-entity? get-entity (:tracker task)) 35 | 36 | (defn survey-component 37 | [task-id owner] 38 | (reify 39 | om/IRenderState 40 | (render-state [_ state] 41 | (with-om-dep-capture owner 42 | (let [card (survey-card (conn owner) task-id) 43 | ts (time-series (conn owner) (:tracker-id card))] 44 | (html 45 | [:h1 "My component"] 46 | [:p (:prompt card)] 47 | (om/build ts/component ts))))))) 48 | 49 | ) 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/derive/simple.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.simple 2 | "Trivial implementation of derive protocol, no performance optimization" 3 | (:refer-clojure :exclude [get-in get]) 4 | (:require [derive.core :as d] 5 | [clojure.set :as set])) 6 | 7 | (defprotocol ISimpleStore 8 | (-update! [store path fov args]) 9 | (get [store key]) 10 | (get-in [store path])) 11 | 12 | (defn path->dep [path] 13 | #_(println path) 14 | (->> path 15 | (map #(subvec path 0 %) 16 | (range 1 (inc (count path)))) 17 | (set))) 18 | 19 | (deftype SimpleStore [a l] 20 | ISimpleStore 21 | (-update! [store path fov args] 22 | (let [res (if (fn? fov) 23 | (if args 24 | (swap! a update-in path #(apply fov %1 args)) 25 | (swap! a update-in path fov)) 26 | (swap! a assoc-in path fov)) 27 | dep (path->dep path)] 28 | (doseq [[listener deps] @l] 29 | #_(println deps dep) 30 | (when (or (nil? deps) (d/match-deps deps dep)) 31 | (listener store dep))) 32 | res)) 33 | 34 | (get [store key] 35 | (d/inform-tracker store (path->dep [key])) 36 | (cljs.core/get @a key)) 37 | 38 | (get-in [store path] 39 | #_(println path) 40 | (d/inform-tracker store (path->dep path)) 41 | (cljs.core/get-in @a path)) 42 | 43 | IDeref 44 | (-deref [store] @a) 45 | 46 | d/IDependencySource 47 | (subscribe! [this listener] 48 | (swap! l assoc listener nil)) 49 | (subscribe! [this listener deps] 50 | (swap! l assoc listener deps)) 51 | (unsubscribe! [this listener] 52 | (swap! l dissoc listener)) 53 | (unsubscribe! [this listener deps] 54 | (swap! l dissoc listener)) 55 | (empty-deps [this] #{})) 56 | 57 | (defn update! [store path fov & args] 58 | (-update! store path fov args)) 59 | 60 | (defn create 61 | ([] (create {})) 62 | ([init] (SimpleStore. (atom init) (atom {})))) 63 | 64 | -------------------------------------------------------------------------------- /test/derive/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.runner 2 | (:require [derive.simple-test] 3 | [cljs.test :refer-macros [run-tests] :as test])) 4 | 5 | (set! *print-newline* false) 6 | (set-print-fn! #(js/console.log %)) 7 | 8 | (def report (atom nil)) 9 | 10 | (defn run-all-tests 11 | [] 12 | (.log js/console "Running all tests") 13 | (run-tests (test/empty-env) 14 | 'derive.simple-test) 15 | (test/successful? @report)) 16 | 17 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] 18 | (if (test/successful? m) 19 | (println "cljs.test/report -> Tests Succeeded!") 20 | (do 21 | (reset! report m) 22 | (println "cljs.test/report -> Tests Failed :(") 23 | (prn m)))) 24 | -------------------------------------------------------------------------------- /test/derive/simple_test.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.simple-test 2 | (:require [clojure.set :as set] 3 | [cljs.test :as t :refer-macros [is deftest]] 4 | [derive.core :as d :refer-macros [defnd with-tracked-dependencies on-changes]] 5 | [derive.simple :as store])) 6 | 7 | (deftest path-to-dep 8 | (is (= (store/path->dep [:a :b :c]) 9 | #{[:a] [:a :b] [:a :b :c]}))) 10 | 11 | (deftest store-access 12 | (let [store (store/create {:a {:b {:c 1}}})] 13 | (is (= (store/get store :a) {:b {:c 1}})) 14 | (is (= (store/get-in store [:a :b :c]) 1)))) 15 | 16 | (deftest store-update 17 | (let [store (store/create)] 18 | (store/update! store [:a] 10) 19 | (is (= (store/get store :a) 10)) 20 | 21 | (store/update! store [:b :c] (constantly 20)) 22 | (is (= (store/get-in store [:b :c]) 20)) 23 | 24 | (store/update! store [:b :d] (fn [old new] new) 30) 25 | (is (= (store/get-in store [:b :d]) 30)))) 26 | 27 | (defn dval [store path] 28 | (store/get-in store path)) 29 | 30 | (deftest store-tracking 31 | (let [store (store/create {:a 1 :b 2})] 32 | (is (= (dval store [:a]) 1)) 33 | (store/update! store [:a] 3) 34 | (is (= (dval store [:a]) 3)) 35 | (is (= (dval store [:b]) 2)))) 36 | 37 | 38 | (deftest set-deps 39 | (let [d1 #{:b :c} 40 | d2 (d/merge-deps #{:a} d1)] 41 | (is (= d2 #{:a :b :c})) 42 | (is (= (d/match-deps d1 d2))) 43 | (is (not (d/match-deps d1 #{:a}))))) 44 | 45 | 46 | (deftest cache 47 | (let [c (d/default-cache)] 48 | (d/add-value! c [1 2] :result {:store #{1 4 8}}) 49 | (is (= (first (d/get-value c [1 2])) :result)) 50 | (is (= (d/invalidate! c :store #{4}) [[1 2]])))) 51 | 52 | (deftest simple-store 53 | (let [store (store/create) 54 | res (atom nil) 55 | handler (fn [store deps] 56 | (reset! res deps))] 57 | (d/subscribe! store handler #{[3 4]}) 58 | (store/update! store [3 4] 10) 59 | (is (= (store/get-in store [3 4]) 10)) 60 | (is (set/intersection @res #{[3 4]})) 61 | (store/update! store [1 2] 20) 62 | (is (set/intersection @res #{[3 4]})))) 63 | 64 | (deftest simple-store-tracking1 65 | (let [store (store/create) 66 | query-deps (atom nil) 67 | tracker-handler (fn [store deps] (reset! query-deps deps))] 68 | (doto store 69 | (store/update! [1 2] 3) 70 | (store/update! [3 4] 5) 71 | (store/update! [1 3] 5)) 72 | (is (= @store {1 {2 3 3 5} 3 {4 5}})) 73 | (with-tracked-dependencies [tracker-handler] 74 | (is (= (store/get-in store [1 2]) 3)) 75 | (is (= (store/get-in store [1 3]) 5))) 76 | (is (= (first (vals @query-deps)) #{[1] [1 2] [1 3]})))) 77 | 78 | 79 | (defn- derive-cache-value [df args] 80 | (first (d/get-value (.-cache df) args))) 81 | 82 | (defnd dvald [store path] 83 | (store/get-in store path)) 84 | 85 | (deftest derive1 86 | (let [store (store/create {:a 1 :b 2})] 87 | ;; Can read 88 | (is (= (dvald store [:a]) 1)) 89 | ;; Read is pulled from dvald cache 90 | (is (= (derive-cache-value dvald [store [:a]]) 1)) 91 | (store/update! store [:a] 3) 92 | ;; Cache was invalidated 93 | (is (= (derive-cache-value dvald [store [:a]]) nil)) 94 | (is (= (dvald store [:a]) 3)) 95 | ;; Cache was restored 96 | (is (= (derive-cache-value dvald [store [:a]]) 3)) 97 | (store/update! store [:b] 3) 98 | ;; Original cache value remains untouched 99 | (is (= (derive-cache-value dvald [store [:a]]) 3)) 100 | (is (= (dvald store [:b]) 3)))) 101 | 102 | 103 | (defnd d1 [store path mul] 104 | (* mul (store/get-in store path))) 105 | 106 | (defnd rootd [store val] 107 | (+ (d1 store [1 val] 3) 108 | (d1 store [2 val] 4))) 109 | 110 | (deftest nested-derive 111 | (let [store (store/create {1 {1 2 2 20} 2 {1 4 2 40}})] 112 | (is (= (rootd store 1) (+ 6 16))) 113 | (is (= (derive-cache-value rootd [store 1]) (+ 6 16))) 114 | (is (= (derive-cache-value d1 [store [1 1] 3]) 6)) 115 | (is (= (derive-cache-value d1 [store [2 1] 4]) 16)) 116 | (store/update! store [1 1] 3) 117 | ;; Chain of deps is invalidated 118 | (is (= (derive-cache-value rootd [store 1]) nil)) 119 | (is (= (derive-cache-value d1 [store [1 1] 3]) nil)) 120 | (is (= (derive-cache-value d1 [store [2 1] 4]) 16)))) 121 | 122 | (deftest changes 123 | (let [store (store/create {1 :test1}) 124 | id 1 125 | owner (js-obj) 126 | target (atom nil) 127 | render (fn [store id] 128 | (reset! target (store/get store id)))] 129 | (on-changes [ (d/om-subscribe-handler owner) 130 | #(render store id) ] 131 | (render store id)) 132 | (is (= @target :test1)) 133 | (store/update! store [1] :test2) 134 | (is (= @target :test2)) 135 | (store/update! store [1] :test3) 136 | (is (= @target :test3)))) 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /test/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Ap = Array.prototype; 4 | var slice = Ap.slice; 5 | var Fp = Function.prototype; 6 | 7 | if (!Fp.bind) { 8 | // PhantomJS doesn't support Function.prototype.bind natively, so 9 | // polyfill it whenever this module is required. 10 | Fp.bind = function(context) { 11 | var func = this; 12 | var args = slice.call(arguments, 1); 13 | 14 | function bound() { 15 | var invokedAsConstructor = func.prototype && (this instanceof func); 16 | return func.apply( 17 | // Ignore the context parameter when invoking the bound function 18 | // as a constructor. Note that this includes not only constructor 19 | // invocations using the new keyword but also calls to base class 20 | // constructors such as BaseClass.call(this, ...) or super(...). 21 | !invokedAsConstructor && context || this, 22 | args.concat(slice.call(arguments)) 23 | ); 24 | } 25 | 26 | // The bound function must share the .prototype of the unbound 27 | // function so that any object created by one constructor will count 28 | // as an instance of both constructors. 29 | bound.prototype = func.prototype; 30 | 31 | return bound; 32 | }; 33 | } 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /test/phantomjs.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var url,args; 3 | 4 | if (phantom.version.major > 1) { 5 | args = system.args; 6 | if (args.length < 2) { 7 | system.stderr.write('Expected a target URL parameter.'); 8 | phantom.exit(1); 9 | } 10 | url = args[1]; 11 | } else { 12 | args = phantom.args; 13 | if (args.length < 1) { 14 | system.stderr.write('Expected a target URL parameter.'); 15 | phantom.exit(1); 16 | } 17 | url = args[0]; 18 | } 19 | 20 | var page = require('webpage').create(); 21 | 22 | page.onConsoleMessage = function (message) { 23 | console.log("Console: " + message); 24 | }; 25 | 26 | console.log("Loading URL: " + url); 27 | 28 | page.open(url, function (status) { 29 | if (status != "success") { 30 | console.log('Failed to open ' + url); 31 | phantom.exit(1); 32 | } 33 | console.log('Opened ' + url); 34 | 35 | var result = page.evaluate(function () { 36 | return derive.runner.run_all_tests(); 37 | }); 38 | 39 | // NOTE: PhantomJS 1.4.0 has a bug that prevents the exit codes 40 | // below from being returned properly. :( 41 | // 42 | // http://code.google.com/p/phantomjs/issues/detail?id=294 43 | 44 | if ( result ) { 45 | console.log("PhantomJS runner: Success."); 46 | phantom.exit(0); 47 | } else { 48 | console.log("PhantomJS runner: *** Tests failed! ***"); 49 | phantom.exit(1); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /todo.cljs: -------------------------------------------------------------------------------- 1 | (ns derive.todo 2 | (:require-macros [secretary.macros :refer [defroute]]) 3 | (:require [goog.events :as events] 4 | [om.core :as om :include-macros true] 5 | [sablono.core :as html :refer-macros [html]] 6 | [secretary.core :as secretary] 7 | [clojure.string :as string] 8 | [derive.nativestore :as store :refer [insert! delete! cursor transact!]] 9 | [derive.dfns :as d]) 10 | (:import [goog History] 11 | [goog.history EventType])) 12 | 13 | (enable-console-print!) 14 | 15 | (def ENTER_KEY 13) 16 | 17 | (defonce db (store/native-store)) 18 | (defonce app-state (atom [db])) 19 | 20 | ;; 21 | ;; Main view state 22 | ;; 23 | 24 | (defn view [db] 25 | (db "root")) 26 | 27 | ;; Actions 28 | (defn filter-list! [db filter] 29 | (insert! db #js {:id "root" :filter (keyword filter)})) 30 | 31 | (defn list-filter [db] 32 | (:filter (db "root"))) 33 | 34 | ;; 35 | ;; Todos 36 | ;; 37 | 38 | (defn toggle-all! [db checked] 39 | (->> (fn [db] 40 | (d/reducec->> 41 | (cursor db :type :todo :todo) 42 | (r/map #(insert! db #js {:id (:id %) :completed checked})))) 43 | (transact! db))) 44 | 45 | (defn destroy-todo! [db todo-id] 46 | (delete! db id)) 47 | 48 | (defn create-todo! [db ] 49 | 50 | (defn handle-events [db type val] 51 | (case type 52 | :destroy (destroy-todo! db val))) 53 | 54 | 55 | ;; 56 | ;; Routes 57 | ;; 58 | 59 | 60 | (defroute "/" [] (filter-list! :all) 61 | 62 | (defroute "/:filter" [filter] (filter-list! filter)) 63 | 64 | ;; 65 | ;; Init 66 | ;; 67 | 68 | (defn load-db [] 69 | (ensure-index :type) 70 | (filter-list! :all)) 71 | 72 | 73 | (declare toggle-all) 74 | 75 | (defn visible? [todo filter] 76 | (case filter 77 | :all true 78 | :active (not (:completed todo)) 79 | :completed (:completed todo))) 80 | 81 | 82 | (defn-derived main-state [db] 83 | (let [todo (cursor db :type :todo)] 84 | 85 | [todos showing editing] :as app} 86 | 87 | 88 | (defhtml main [[todo showing editing] comm] 89 | (let [[todos showing editing] (main-state db)] 90 | [:section #js {:id "main" :style (hidden (empty? todos))} 91 | (dom/input 92 | #js {:id "toggle-all" :type "checkbox" 93 | :onChange #(toggle-all % app) 94 | :checked (every? :completed todos)}) 95 | (apply dom/ul #js {:id "todo-list"} 96 | (om/build-all item/todo-item todos 97 | {:init-state {:comm comm} 98 | :key :id 99 | :fn (fn [todo] 100 | (cond-> todo 101 | (= (:id todo) editing) (assoc :editing true) 102 | (not (visible? todo showing)) (assoc :hidden true)))})))) 103 | 104 | (defn make-clear-button [completed comm] 105 | (when (pos? completed) 106 | (dom/button 107 | #js {:id "clear-completed" 108 | :onClick #(put! comm [:clear (now)])} 109 | (str "Clear completed (" completed ")")))) 110 | 111 | (defn footer [app count completed comm] 112 | (let [clear-button (make-clear-button completed comm) 113 | sel (-> (zipmap [:all :active :completed] (repeat "")) 114 | (assoc (:showing app) "selected"))] 115 | (dom/footer #js {:id "footer" :style (hidden (empty? (:todos app)))} 116 | (dom/span #js {:id "todo-count"} 117 | (dom/strong nil count) 118 | (str " " (pluralize count "item") " left")) 119 | (dom/ul #js {:id "filters"} 120 | (dom/li nil (dom/a #js {:href "#/" :className (sel :all)} "All")) 121 | (dom/li nil (dom/a #js {:href "#/active" :className (sel :active)} "Active")) 122 | (dom/li nil (dom/a #js {:href "#/completed" :className (sel :completed)} "Completed"))) 123 | clear-button))) 124 | 125 | 126 | 127 | 128 | 129 | 130 | --------------------------------------------------------------------------------