├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------