├── circle.yml
├── .gitignore
├── resources
└── test
│ └── index.html
├── test
├── nativestore
│ ├── runner.cljs
│ └── internal.cljs
├── phantomjs-shims.js
└── phantomjs.js
├── src
└── nativestore
│ ├── dfns.cljs
│ ├── core.cljs
│ └── heap.cljs
├── project.clj
└── README.md
/circle.yml:
--------------------------------------------------------------------------------
1 | test:
2 | override:
3 | - lein with-profiles base,test cljsbuild test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | pom.xml.asc
3 | *jar
4 | /lib/
5 | /classes/
6 | /target/
7 | /checkouts/
8 | .lein-deps-sum
9 | .lein-repl-history
10 | .lein-plugins/
11 | .lein-failures
12 | /.nrepl-port
13 | resources/test/js
14 |
--------------------------------------------------------------------------------
/resources/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Nativestore Test
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/nativestore/runner.cljs:
--------------------------------------------------------------------------------
1 | (ns nativestore.runner
2 | (:require [nativestore.internal]
3 | [nativestore.core]
4 | [cljs.test :refer-macros [run-tests] :as test]))
5 |
6 | (set! *print-newline* false)
7 | (set-print-fn! #(js/console.log %))
8 |
9 | (def report (atom nil))
10 |
11 | (defn run-all-tests
12 | []
13 | (.log js/console "Running all tests")
14 | (run-tests (test/empty-env)
15 | 'nativestore.internal)
16 | (test/successful? @report))
17 |
18 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
19 | (if (test/successful? m)
20 | (println "cljs.test/report -> Tests Succeeded!")
21 | (do
22 | (reset! report m)
23 | (println "cljs.test/report -> Tests Failed :(")
24 | (prn m))))
25 |
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/nativestore/dfns.cljs:
--------------------------------------------------------------------------------
1 | (ns derive.dfns
2 | (:require [clojure.core.reducers :as r])
3 | (:refer-clojure :exclude [filter map mapcat count remove sort sort-by]))
4 |
5 | (extend-protocol IReduce
6 | array
7 | (-reduce
8 | ([coll f]
9 | (areduce coll i r (f) (f r (aget coll i))))
10 | ([coll f start]
11 | (areduce coll i r start (f r (aget coll i))))))
12 |
13 | (defn js-conj
14 | ([] #js [])
15 | ([val] #js [val])
16 | ([arry val] (do (.push arry val) arry)))
17 |
18 | (extend-protocol ITransientCollection
19 | array
20 | (-conj! [arry val] (js-conj arry val))
21 | (-persistent! [tcoll] tcoll))
22 |
23 | (defn count
24 | ([f coll]
25 | (reduce #(inc %1) 0 coll)))
26 |
27 | (defn reduce->>
28 | [coll & forms]
29 | (r/reduce (last forms) ((apply comp (reverse (butlast forms))) coll)))
30 |
31 | (defn reducec->>
32 | [coll & forms]
33 | (if (> (cljs.core/count forms) 0)
34 | (r/reduce js-conj #js [] ((apply comp (reverse forms)) coll))
35 | (r/reduce js-conj #js [] coll)))
36 |
37 | (defn sort
38 | ([coll] (.sort coll compare))
39 | ([comp coll] (.sort coll comp)))
40 |
41 | (defn sort-by
42 | ([keyfn coll] (.sort coll #(compare (keyfn %1) (keyfn %2))))
43 | ([keyfn comp coll] (.sort coll #(comp (keyfn %1) (keyfn %2)))))
44 |
45 |
46 | ;;
47 | ;; Non-reduce native helpers
48 | ;;
49 |
50 | (defn sort-in-place [arry f]
51 | (.sort arry f))
52 |
--------------------------------------------------------------------------------
/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 nativestore.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 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject com.vitalreactor/nativestore "0.2.3-SNAPSHOT"
2 | :description "A client-side, in-memory, indexed data store."
3 | :url "http://github.com/vitalreactor/nativestore"
4 | :license {:name "MIT License"
5 | :url "http://github.com/vitalreactor/derive/blob/master/LICENSE"}
6 | :dependencies [[org.clojure/clojure "1.7.0"]
7 | [org.clojure/clojurescript "1.8.51"]
8 | [prismatic/schema "0.2.6"]
9 | [com.vitalreactor/derive "0.2.1"]]
10 | :plugins [[lein-cljsbuild "1.1.2"]]
11 | :hooks [leiningen.cljsbuild]
12 | :profiles {:dev {:dependencies [[org.clojure/tools.nrepl "0.2.4"]]
13 | :source-paths ["src" "test"]}}
14 | :cljsbuild {:builds
15 | [ {:id "test"
16 | :source-paths ["src" "test"]
17 | :compiler {:main orchestra.runner
18 | :output-to "resources/test/js/testable.js"
19 | :output-dir "resources/test/js/out"
20 | :source-map "resources/test/js/testable.js.map"
21 | :asset-path "/js/out"
22 | :parallel-build true
23 | :optimizations :whitespace
24 | :recompile-dependents false
25 | :pretty-print true}}]
26 | :test-commands {"all" ["phantomjs" "test/phantomjs.js" "resources/test/index.html"]}})
27 |
28 |
--------------------------------------------------------------------------------
/test/nativestore/internal.cljs:
--------------------------------------------------------------------------------
1 | (ns nativestore.internal
2 | (:require [cljs.test :as t :refer-macros [deftest is]]
3 | [clojure.set :as set]
4 | [derive.core :refer-macros [defnd with-tracked-dependencies]]
5 | [derive.core :as d]
6 | [nativestore.core :as store]))
7 |
8 | (defn insert-population [store]
9 | (doto store
10 | (store/insert! #js {:id 1 :type "user" :name "Fred" :income 10})
11 | (store/insert! #js {:id 2 :type "user" :name "Zoe" :income 10})
12 | (store/insert! #js {:id 3 :type "user" :name "Apple" :income 20})
13 | (store/insert! #js {:id 4 :type "user" :name "Flora" :income 20})
14 | (store/insert! #js {:id 5 :type "user" :name "Flora" :income 10})
15 | (store/insert! #js {:id 6 :type "super" :name "George" :income 5})))
16 |
17 | (deftest native-type
18 | (is (store/read-only? (store/native true)))
19 | (is (not (store/read-only? (store/native false))))
20 | (let [sample (store/native false)]
21 | ))
22 |
23 | (deftest basic-store
24 | (let [store (store/create)]
25 | (store/ensure-index store :name :name)
26 | (->> (store/compound-index [:income :name] [compare compare])
27 | (store/ensure-index store :income-alpha))
28 | (insert-population store)
29 | (is (= (:name (get store 1)) "Fred"))
30 | (is (= (:name (store 1)) "Fred"))
31 |
32 | ;; With a DB index
33 | (is (= (set (mapv :id (store/cursor store))) #{1 2 3 4 5 6}))
34 |
35 | ;; With a simple index
36 | (is (= (set (mapv :id (store/cursor store :name "Flora" "Flora"))) #{4 5}))
37 | (is (= (set (mapv :id (store/fetch store :name "Flora"))) #{4 5}))
38 |
39 | ;; With a compound index
40 | (is (= (mapv :id (store/cursor store :income-alpha)) [6 5 1 2 3 4]))
41 | (is (= (mapv :id (store/cursor store :income-alpha #js [10 "Fred"] #js [20 "Apple"]))
42 | [1 2 3]))
43 |
44 | ;; Test deletion
45 | (store/delete! store 2)
46 | (is (nil? (store 2)))
47 | (is (= (mapv :id (store/cursor store :income-alpha)) [6 5 1 3 4]))))
48 |
49 | (deftest references
50 | (let [store (store/create)]
51 | (doto store
52 | (store/insert! #js {:id 1 :type :account :name "Ian" :friend (store/reference store 2)})
53 | (store/insert! #js {:id 2 :type :account :name "Fred" :friend (store/reference store 3)})
54 | (store/insert! #js {:id 3 :type :account :name "Pam"
55 | :children #js [(store/reference store 4) (store/reference store 5)]})
56 | (store/insert! #js {:id 4 :type :account :name "Jack"})
57 | (store/insert! #js {:id 5 :type :account :name "Jill"}))
58 | (is (= (:name (:friend (store 1))) (:name (store 2))))
59 | (is (= (:name (:friend (store 2))) (:name (store 3))))
60 | (is (= (:name (get-in (store 1) [:friend :friend])) "Pam"))
61 | (is (= (mapv :name (:children (store 3))) ["Jack" "Jill"]))))
62 |
63 |
64 | (defnd income [store value]
65 | (mapv :id (store/cursor store :income value value)))
66 |
67 | (defnd by-name [store nam]
68 | (mapv :id (store/cursor store :name nam nam)))
69 |
70 | (deftest derive-integration
71 | (let [store (store/create)]
72 | (store/ensure-index store :name :name)
73 | (store/ensure-index store :income :income)
74 | (insert-population store)
75 | (with-tracked-dependencies [(fn [a b] nil)]
76 | (is (= (count (by-name store "Flora")) 2))
77 | (is (= (count (by-name store "Apple")) 1))
78 | (is (= (count (income store 10)) 3))
79 | (is (= (count (income store 20)) 2))
80 | (is (= (count (income store 5)) 1))
81 | (is (= (count (income store 0)) 0)))))
82 |
83 |
84 | ;(deftest multi-index
85 | ; (let [idx (store/multi-index :a comparator)]
86 | ; (store/insert! idx (js-obj :a [1 2] :name "one-two"))
87 | ; (store/insert! idx (js-obj :a [3 4] :name "three-four"))
88 | ; (store/insert! idx (js-obj :a [2 4] :name "even"))
89 | ; (is (= (set (into [] (store/cursor idx 2 3)))
90 | ; #{"one-two" "three-four" "even"}))))
91 |
92 |
93 | (deftest clear-db
94 | (let [store (store/create)]
95 | (store/ensure-index store :name :name)
96 | (insert-population store)
97 | (is (= (:id (store 2)) 2)
98 | (is (= (count (by-name store "Flora")) 2)))
99 | (store/clear! store)
100 | (is (nil? (:id (store 2))))
101 | (is (= (count (by-name store "Flora")) 0))))
102 |
103 |
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | NativeStore
2 | ===========
3 |
4 | NativeStore is an explicitly indexed in-memory datastore for managing
5 | native objects. It supports the
6 | [Derive](http://github.com/vitalreactor/derive) library's dependency
7 | tracking protocol.
8 |
9 | NativeStore is best thought of as an in-memory heap with database-like
10 | semantics on which one or more ordered indexes can be defined.
11 | Objects can be insert!'ed and remove!'ed. Insert operations have
12 | upsert-merge semantics, combining the key-value set of the new object
13 | with the existing state. The transaction! function enables you to
14 | perform a set of updates and inform listeners of the aggregate updates
15 | rather than on each primitive insert! operation.
16 |
17 | ## Tutorial
18 |
19 | The best way to become familiar with native store
20 |
21 | ```clj
22 | user=> (require '[nativestore.core :as store])
23 | ```
24 |
25 | ### Store Interface
26 |
27 | NativeStore indexes a heap of native javascript objects uniquely
28 | identified by a root key "id". It has a simple public API.
29 |
30 | Add, removing, and retrieve objects:
31 |
32 | ```clj
33 | (let [store (store/create)] ;; Returns a mutable store object
34 | (store/insert! store obj) ;; Insert object in DB and return
35 | (store/delete! store id) ;; Delete object by ID
36 | (store/transact! store fn) ;; Batch multiple inserts/deletes
37 | (get store id) ;; Return object for id
38 | (store id) ;; A store implements IKeywordLookup
39 | ```
40 |
41 | Declare and use an index on the store. Objects that return nil when
42 | passed to key-fn are not indexed.
43 |
44 | ```clj
45 | (store/ensure-index store name key-fn) ;; Declare an index with default sorting on key
46 | (store/ensure-index store name key-fn comparator-fn)
47 | (store/ensure-index store name index) ;; Add an explicit index (see below)
48 | ```
49 |
50 | You'll want to use the explicit index form for things like compound
51 | indexes. An ordered index is the default.
52 |
53 | ```clj
54 | user=> (->> (store/ordered-index (store/field-key :lastname) compare)
55 | (store/ensure-index store :lastname))
56 |
57 | user=> (->> (store/compound-index [:lastname :age] [compare >])
58 | (store/ensure-index store :name-age))
59 | ```
60 |
61 | ### Store Cursors
62 |
63 | Indexes are based on the Closure library's binary search operation
64 | over arrays or standard JS object hashes. The only public interface
65 | to indexes is a Cursor object. A Cursor maintains pointers into the
66 | materialized array index and provides read-only access to the collection.
67 |
68 | Cursors are reducable (implement IReduce) so you can use them with
69 | transducers and standard seq operations such as map, into, etc. They
70 | do not, however, implement the seq protocol as they are a wrapper
71 | around a native read-only array. Cursors are not guaranteed to be
72 | valid across transaction/insert! boundaries so should be used in
73 | contexts with finite extent.
74 |
75 | ```clj
76 | (store/cursor store) ;; A collection over the whole store
77 | (store/cursor store index-name) ;; A collection over the whole index
78 | (store/cursor store index-name start) ;; Collection over index starting at start
79 | (store/cursor store index-name start end) ;; Range over the index
80 | ```
81 |
82 | Some examples of use:
83 |
84 | ```clj
85 | (store/insert! store #js {:id 1 :firstname "Joe" :lastname "Smith"})
86 | (store/insert! store #js {:id 2 :firstname "Fred" :lastname "Savage"})
87 | (store/insert! store #js {:id 3 :firstname "Larry" :lastname "Stooge"})
88 | (store/insert! store #js {:id 4 :firstname "Curly" :lastname "Stooge"})
89 | (store/insert! store #js {:id 5 :firstname "Mo" :lastname "Stooge"})
90 | (store/insert! store #js {:id 6 :firstname "Reginald" :lastname "Quince"})
91 |
92 | (->> (store/cursor :name)
93 | (into []))
94 |
95 | (let [xform (comp (map :firstname) (map count))]
96 | (into [] xform (store/cursor :name)))
97 |
98 |
99 | (defn running-avg
100 | "Reducing function for computing the running average (mutates)"
101 | ([] (js-obj "sum" 0 "cnt" 0))
102 | ([avg] (/ (aget avg "sum") (aget avg "cnt")))
103 | ([avg val]
104 | (aset avg "sum" (+ (aget avg "sum") val))
105 | (aset avg "cnt" (inc (aget avg "cnt")))
106 | avg))
107 |
108 | (defn td-avg-string-len
109 | "Transducer that computes average string length of a collection"
110 | [key coll]
111 | (let [xform (comp (map key) (remove empty?) (map count))]
112 | (->> (transduce xform running-avg coll)
113 | (running-avg))))
114 |
115 | (->> (store/cursor :name)
116 | (td-avg-string-len :firstname))
117 | ```
118 |
119 | ### The Native Type
120 |
121 | When you add an object to a store, NativeStore upgrades its type to native
122 | which operates just like a native object, but adds a few features
123 | and protocol implementations.
124 |
125 | ```clj
126 | user=> (def obj (as-native #js {:firstname "John" :lastname "Smith" :age 23}))
127 | #native {:firstname "John" :lastname "Smith" :age 23}
128 |
129 | user=> (:firstname obj) ;; Keyword access
130 | "John"
131 |
132 | user=> (obj :firstname) ;; Map-like interface
133 | "John"
134 |
135 | user=> (assoc obj :firstname "Fred") ;; via shallow clone
136 | #native {:firstname "Fred" :lastname "Smith" :age 23}
137 |
138 | user=> obj
139 | #native {:firstname "John" :lastname "Smith" :age 23}
140 |
141 | user=> (assoc! obj :firstname "Johnny") ;; mutating
142 | #native {:firstname "Johnny" :lastname "Smith" :age 23}
143 |
144 | user=> obj
145 | #native {:firstname "Johnny" :lastname "Smith" :age 23}
146 |
147 | user=> (store 1)
148 | #native {:id 1 :firstname "Joe" :lastname "Smith"}
149 |
150 | user=> (assoc! (store 1) :firstname "Joseph")
151 | ;; ERROR! The store returns read only natives
152 |
153 | user=> (assoc (store 1) :firstname "Joseph")
154 | #native {:id 1 :firstname "Joeseph" :lastname "Smith"}
155 |
156 | ```
157 |
158 | Native objects support the common clojurescript interfaces countable,
159 | assocable, transient, and lookup. Standard assoc operations return
160 | _shallow_ clones of objects with the new change. assoc! is
161 | an abstraction over aset.
162 |
163 | Native objects stored in the store are marked read-only on insertion.
164 | When native objects are cloned the read-only setting is cleared,
165 | allowing assoc'ed copies to be freely mutated by downstream code. The
166 | database state object can be updated by calling insert! on the
167 | store with the mutated object.
168 |
169 | The intent with Native objects is to encourage the use of Clojure
170 | idioms on native objects so we can enforce constraints like read-only
171 | (to avoid pernicious changes to the datastore) and dependency tracking
172 | when accessing referenced objects.
173 |
174 | ## References
175 |
176 | We also provide a Reference type to simplify modeling state that has a
177 | relational or graph-like structure. When the clojure API is used to
178 | access a Native attribute, if the resulting value implements the
179 | IReference protocol, the reference is followed and the referenced
180 | value returned instead. This is implemented as a deftype which
181 | maintains a reference to the store value and the root ID to lookup
182 | ("id").
183 |
184 | References are usually setup at import time:
185 |
186 | ```clj
187 | user=> (store/insert! store #js {:id 104 :firstname "Sally" :lastname "Smith"
188 | :father (store/reference 101)})
189 | user=> (store/insert! store #js {:id 105 :firstname "Johnny" :lastname "Smith"
190 | :father (store/reference 101)})
191 | user=> (store/insert! store
192 | #js {:id 101 :firstname "Joe" :lastname "Smith"
193 | :children [(store/reference store 104)
194 | (store/reference store 105)]})
195 | ```
196 |
197 | References are supported by the keyword access protocol and unpacked when
198 | read by looking up the current value of the object in the database.
199 |
200 | ```clj
201 | user=> (get-in (store 104) [:father :firstname])
202 | "Joe"
203 |
204 | user=> (mapv :firstname (:children (store 101)))
205 | ["Sally" "Johnny"]
206 | ```
207 |
208 | # Derive Integration
209 |
210 | One of the design goals for NativeStore was to provide a foundation for
211 | the concepts in the [Derive](http://github.com/vitalreactor/derive) library.
212 | Because maintaining copies of mutable objects can be error prone, we would like
213 | to carefully bound the lifecycle of a set of queried objects so they are not
214 | used again unless they are guaranteed to be "current". (Datomic-style immutable value
215 | semantics are not a goal of NativeStore, although history and recoverability
216 | may be -- see below).
217 |
218 | Derive helps us define functions using the macro `defnd` that always
219 | return a value computed from the latest state of the database, but
220 | without recomputing the result each time. Instead, the function
221 | subscribes to updates from the database and invalidates results iff
222 | the query result would depend on the latest changes.
223 |
224 | ```clj
225 | (defnd note-view [store note-id]
226 | (let [note (store note-id)]
227 | (-> note
228 | (update-in :content render-markdown) ;; make a copy
229 | (assoc! :last-edited (date-str (:last-edited note))))))
230 | ```
231 |
232 | This function memoizes the result of the call along with the dependencies of
233 | the query (here just the note-id) reported by the store during the execution
234 | of the defnd's body. Callers of defnd, such as an Om render method, can use
235 | `on-changes` to capture dynamic dependences and invoke a callback when any
236 | of those dependencies change. e.g.
237 |
238 | ```clj
239 | (render [_]
240 | (derive/on-changes [(derive/om-subscribe-handler owner) #(om/refresh! owner)]
241 | (render-note (note-view (om/get-shared owner :store) note-id))))
242 | ```
243 |
244 | The Om subscription handler manages storing the handler and clearing
245 | it when a new callback is being subscribed. You can also implement a
246 | will-unmount handler using `derive/om-unsubscribe-component` to
247 | unregister the callback when the component is re-rendered or unmounted
248 | to avoid memory leaks. The refresh function is called when new
249 | dependencies are written.
250 |
251 | Here is a more complete derive function involving copying and mutation
252 | of the retrieved data.
253 |
254 | ```
255 | (defnd note-view [store note-id]
256 | (let [note (db note-id)
257 | sender (:sender note)] ;; via native refs
258 | (-> note
259 | (assoc :note-class (f-of-note note) ;; ensure a copy
260 | (update-in! :date human-readable) ;; mutation
261 | (assoc! :content escape-content))))) ;; mutation
262 | ```
263 |
264 | Overwriting the original note would pullute the DB in an untrackable
265 | way so is an error (via the read-only flag on natives) but the copy is
266 | mutable. Attribute references that use the reference object also
267 | participate in the dependency tracking protocol.
268 |
269 |
270 | # Caveat emptor
271 |
272 | The immutable abstraction of NativeStore is not rigorously enforced.
273 | Some critical things you should keep in mind:
274 |
275 | - Transactions are not ACID. They are Consistent and Isolated (by
276 | virtue of javascript's single-threadedness). While side effects are
277 | computed atomically, they are not strictly Atomic as errors will
278 | leave the database in an inconsistent state. There are no
279 | persistence or Durability guarantees either.
280 | - It is currently an error to mutate a database object within a transaction
281 | function without calling insert! to update the indices. In the future
282 | we will track reads and conservatively assume that the transaction
283 | side effects any object it touches.
284 | - Native objects may not be mutated outside transactions unless they are
285 | first copied. This is to inhibit side effects to the DB outside of transaction
286 | functions and insert!/remove!.
287 | - Object identity is retained across inserts (changes are written into
288 | the original object), but code should not depend on identity or '='
289 | remaining valid across transactions.
290 | - Database cursors are invalidated by transactions. There are
291 | currently no checks for cursor invalidation, so it is best to use
292 | cursors in contexts with finite extent.
293 | - Conventions are only enforced if you stick to clojure interfaces.
294 | aget, aset, and direct member access bypass reference resolution,
295 | read-only enforcemenet, etc. However, if you kow what you are doing
296 | you can still access values using constructs like (aget native
297 | "field") and get near-optimal read performance on natives.
298 | - The store does not currently maintain historical versioning or a
299 | transaction log, although future versions are likely to support these
300 | features to enable efficient snapshotting and restoration of system
301 | state.
302 |
303 | All these tradeoffs may be reconsidered in future revisions of the
304 | NativeStore. NativeStore should be considered at an Alpha level of
305 | completeness.
306 |
307 | Use NativeStore responsibly. We emphasized performance with
308 | reasonable flexiblity and safety. Adherence to the conventions
309 | mentioned above is crucial to avoid misusing the tool and creating
310 | difficult to track down bugs.
311 |
312 |
313 |
314 |
315 |
316 |
--------------------------------------------------------------------------------
/src/nativestore/core.cljs:
--------------------------------------------------------------------------------
1 | (ns nativestore.core
2 | (:require [goog.object :as gobj]
3 | [goog.array :as garr]
4 | [derive.core :as d :refer-macros [with-tracked-dependencies defnd]]))
5 |
6 | ;;
7 | ;; Legacy JS wrappers
8 | ;;
9 |
10 | (defn js-strkey [x]
11 | (cond
12 | (string? x) x
13 | (keyword? x) (name x)
14 | :else (str x)))
15 |
16 | (defn js-lookup
17 | ([o k]
18 | (aget o (js-strkey k)))
19 | ([o k not-found]
20 | (let [s (js-strkey k)]
21 | (if-let [res (aget o s)]
22 | res
23 | not-found))))
24 |
25 | (defn js-copy
26 | [o]
27 | (let [t (js/goog.typeOf o)]
28 | (cond (= t "array") (garr/clone o)
29 | :else (gobj/clone o))))
30 |
31 | (defn js-assoc
32 | ([o k v]
33 | (do (aset o (js-strkey k) v)
34 | o))
35 | ([o k v & more]
36 | (js-assoc o k v)
37 | (if more
38 | (recur o (first more) (second more) (nnext more))
39 | o)))
40 |
41 | (defn js-dissoc
42 | [o k & more]
43 | (js-delete o (js-strkey k))
44 | (if more
45 | (recur o (first more) (next more))
46 | o))
47 |
48 | ;;
49 | ;; Store protocols
50 | ;; ============================
51 |
52 | ;; + ILookup
53 | (defprotocol IStore
54 | (insert! [store obj]) ;; shallow merge upsert of native objects
55 | (delete! [store id])) ;; delete, only need primary ID in submitted object
56 |
57 | ;; CompFn(KeyFn(obj)) -> value, obj
58 | (defprotocol IIndex
59 | (key-fn [idx])
60 | (index! [idx obj])
61 | (unindex! [idx obj]))
62 |
63 | (defprotocol IClearable
64 | (clear! [idx]))
65 |
66 | (defprotocol ISortedIndex
67 | (comparator-fn [idx]))
68 |
69 | (defprotocol IScannable
70 | (-get-cursor [idx] [idx start] [idx start end]))
71 |
72 | (defprotocol IIndexedStore
73 | (add-index! [store name index])
74 | (rem-index! [store name])
75 | (get-index [store name]))
76 |
77 | (defprotocol ITransactionalStore
78 | (-transact! [store f args]))
79 |
80 | ;;
81 | ;; Native Dependencies
82 | ;; ============================
83 |
84 | ;; A dependency representation is:
85 | ;;
86 | ;; #js { root: #js [ ...]
87 | ;; : #js [start end] }
88 | ;;
89 | ;; The root is a sorted list of object IDs (reference traversal or direct lookups)
90 | ;; The remaining index lookups maintain value ranges traversed
91 | ;; These become set intersection and range overlap calculations when testing
92 | ;; for the impact of a transaction
93 | ;;
94 | ;; The left side dependency is mutated and returned by all operations
95 | ;;
96 |
97 | (defn- sorted-insert!
98 | "Mutates r1. Keep list of merged IDs in sorted order"
99 | [r1 r2]
100 | (goog.array.forEach r2 (fn [v i a] (goog.array.binaryInsert r1 v))))
101 |
102 | (defn- merge-range!
103 | "Mutates r1. The updated range becomes the union of the two ranges"
104 | [compfn range1 range2]
105 | (let [r1s (aget range1 0)
106 | r1e (aget range1 1)
107 | r2s (aget range2 0)
108 | r2e (aget range2 1)]
109 | (when (< (compfn r2s r1s) 0)
110 | (aset range1 0 r2s))
111 | (when (> (compfn r2e r1e) 0)
112 | (aset range1 1 r2e))))
113 |
114 | (defn- merge-index!
115 | "Merge the index range or root set"
116 | [nset idx range1 range2]
117 | #_(println "idx: " (type idx) "r1: " range1 "r2: " range2 "\n")
118 | (if (nil? idx) ; root?
119 | (sorted-insert! range1 range2)
120 | (merge-range! (comparator-fn idx) range1 range2)))
121 |
122 | (defn- intersect?
123 | "Do two sorted sets of integers intersect?"
124 | [set1 set2]
125 | #_(println "Intersect? " set1 set2)
126 | (let [len1 (if (nil? set1) 0 (alength set1))
127 | len2 (if (nil? set2) 0 (alength set2))]
128 | (loop [i 0 j 0]
129 | (if (or (== i len1) (== j len2))
130 | false
131 | (let [v1 (aget set1 i)
132 | v2 (aget set2 j)]
133 | (cond (= v1 v2) true
134 | (> (compare v1 v2) 0) (recur i (inc j))
135 | :default (recur (inc i) j)))))))
136 |
137 | ;; (0 10) (2 2) => true
138 | ;; (10 20) (5 5) => false
139 | ;; (10 20) (0 10) => true
140 | ;; (10 20) (0 nil) => true
141 | ;; (10 nil) (20 20) => true
142 | (defn- overlap?
143 | "Does the interval of other overlap this?"
144 | [compfn range1 range2]
145 | (let [r1s (aget range1 0)
146 | r1e (aget range1 1)
147 | r2s (aget range2 0)
148 | r2e (aget range2 1)
149 | res (not (or (if (nil? r1e) (< (compfn r2e r1s) 0) (> (compfn r2s r1e) 0))
150 | (if (nil? r2e) (< (compfn r1e r2s) 0) (< (compfn r2e r1s) 0))))]
151 | #_(println "Overlap? " range1 range2 res)
152 | res))
153 |
154 |
155 | (defn- match-index?
156 | [nset idx this-range other-range]
157 | #_(println "Matching index: " this-range " " other-range "\n")
158 | (if (nil? idx) ; root?
159 | (intersect? this-range other-range)
160 | (overlap? (comparator-fn idx) this-range other-range)))
161 |
162 | (deftype NativeDependencySet [store deps]
163 | IPrintWithWriter
164 | (-pr-writer [native writer opts]
165 | (-write writer (str "#ndep [" (pr-str deps) "]")))
166 |
167 | IHash
168 | (-hash [o]
169 | (goog/getUid o))
170 |
171 | IEquiv
172 | (-equiv [o other]
173 | (if (instance? NativeDependencySet other)
174 | (== (-hash o) (-hash other))
175 | false))
176 |
177 | d/IDependencySet
178 | (merge-deps [nset other]
179 | #_(println "NSet merge: " (type store) deps other "\n")
180 | (let [fdeps (if (nil? (.-deps other)) other (.-deps other))]
181 | (goog.object.forEach
182 | fdeps (fn [v k]
183 | (if-let [mine (aget deps k)]
184 | (merge-index! nset (get-index store k) mine v)
185 | (aset deps k (js-copy v)))))
186 | nset))
187 |
188 | (match-deps [nset other]
189 | (let [fdeps (if (nil? (.-deps other)) other (.-deps other))]
190 | #_(println "Matching: " deps fdeps "\n")
191 | (goog.object.some
192 | fdeps (fn [v k o] #_(println "matching-key: " k "\n")
193 | (when-let [local (aget deps k)]
194 | (match-index? nset (get-index store k) local v)))))))
195 |
196 |
197 | (defn make-dependencies
198 | ([store] (NativeDependencySet. store #js {}))
199 | ([store init] (NativeDependencySet. store init)))
200 |
201 | (defn inform-tracker
202 | ([store deps]
203 | (when (d/tracking?)
204 | (inform-tracker d/*tracker* store deps)))
205 | ([tracker store deps]
206 | (let [dset (make-dependencies store deps)]
207 | #_(.log js/console "Informing tracker: " dset " t? " d/*tracker* "\n")
208 | (d/depends! tracker store dset))))
209 |
210 | ;;
211 | ;; Instance protocols
212 | ;; ============================
213 |
214 | (def ^{:doc "Inside a transaction?"
215 | :dynamic true}
216 | *transaction* nil)
217 |
218 | ;; A reference wraps a lookup into a store
219 | ;; Objects implementing ILookup can test for a
220 | ;; IReference and dereference it.
221 |
222 | (defprotocol IReference
223 | (resolve-ref [ref])
224 | (reference-id [ref])
225 | (reference-db [ref]))
226 |
227 | (deftype NativeReference [store id]
228 | IPrintWithWriter
229 | (-pr-writer [native writer opts]
230 | (-write writer (str "#ref [" id "]")))
231 |
232 | IEquiv
233 | (-equiv [ref other]
234 | (and (= store (.-store other))
235 | (= id (.-id other))))
236 |
237 | IReference
238 | (resolve-ref [_] (get store id))
239 | (reference-id [_] id)
240 | (reference-db [_] store))
241 |
242 | (declare native?)
243 |
244 | (defn reference [db obj]
245 | (NativeReference.
246 | db (cond (or (string? obj) (number? obj) (not (native? obj)))
247 | obj
248 | (native? obj)
249 | (let [id ((key-fn (.-root db)) obj)]
250 | (assert id "native object must have an id")
251 | id))))
252 |
253 | (defn identity? [n1 n2]
254 | (= (aget n1 "id") (aget n2 "id")))
255 |
256 | ;;
257 | ;; Low level methods
258 | ;;
259 |
260 | ;; Mutation can only be done on Natives in
261 | ;; a transaction or on copies of Natives generated
262 | ;; via copying assoc or clone
263 |
264 | (defprotocol IReadOnly
265 | (-read-only? [_]))
266 |
267 | (defprotocol INative)
268 |
269 | (deftype Native [__keyset ^:mutable __ro]
270 | INative
271 |
272 | ICloneable
273 | (-clone [this]
274 | (let [clone (Native. (volatile! @__keyset) false)]
275 | (doseq [key @__keyset]
276 | (aset clone (name key) (aget this (name key))))
277 | clone))
278 |
279 | IEmptyableCollection
280 | (-empty [_] (Native. (volatile! #{}) false))
281 |
282 | IReadOnly
283 | (-read-only? [_] __ro)
284 |
285 | ICounted
286 | (-count [native]
287 | (count @__keyset))
288 |
289 | ILookup
290 | (-lookup [native k]
291 | (-lookup native k nil))
292 | (-lookup [native k not-found]
293 | (assert (keyword? k))
294 | (let [v (aget native (name k))]
295 | (cond
296 | (nil? v) not-found
297 | (array? v) (if (satisfies? IReference (aget v 0))
298 | (amap v i ret (resolve-ref (aget v i)))
299 | v)
300 | (satisfies? IReference v) (resolve-ref v)
301 | :default v)))
302 |
303 | ITransientAssociative
304 | (-assoc! [native k v]
305 | (assert (keyword? k))
306 | (when (and (-read-only? native) (not *transaction*))
307 | (throw (js/Error. "Cannot mutate store values outside transact!: ")))
308 | (vswap! __keyset conj k)
309 | (aset native (name k) v)
310 | native)
311 |
312 | ITransientCollection
313 | (-conj! [native [k v]]
314 | (assert (keyword? k))
315 | (-assoc! native k v))
316 |
317 | ITransientMap
318 | (-dissoc! [native k]
319 | (assert (keyword? k))
320 | (when (and (-read-only? native) (not *transaction*))
321 | (throw (js/Error. "Cannot mutate store values outside transact!: ")))
322 | (vswap! __keyset disj k)
323 | (js-delete native (name k))
324 | native)
325 |
326 | IAssociative
327 | (-assoc [native k v]
328 | (assert (keyword? k))
329 | (let [new (clone native)]
330 | (-assoc! new k v)))
331 |
332 | IMap
333 | (-dissoc [native k]
334 | (assert (keyword? k))
335 | (let [new (clone native)]
336 | (-dissoc! new k)))
337 |
338 | ICollection
339 | (-conj [native [k v]]
340 | (assert (keyword? k))
341 | (-assoc native k v))
342 |
343 | ISeqable
344 | (-seq [native]
345 | (map (fn [k] [(keyword k) (aget native (name k))]) @__keyset))
346 |
347 | IEncodeClojure
348 | (-js->clj [native opts]
349 | native)
350 |
351 | IPrintWithWriter
352 | (-pr-writer [native writer opts]
353 | (-write writer "#native ")
354 | (print-map (-seq native)
355 | pr-writer writer opts)))
356 |
357 | (defn native
358 | "Return a fresh native, optionally with the read-only set"
359 | ([] (native false))
360 | ([ro?] (Native. (volatile! #{}) ro?)))
361 |
362 | (defn native?
363 | "Is this object a #native?"
364 | [native]
365 | (satisfies? INative native))
366 |
367 | (defn to-native
368 | "Copying conversion function, will return
369 | a fresh, writable, #native"
370 | [obj]
371 | (cond (native? obj) (clone obj)
372 | (object? obj) (let [native (native false)]
373 | (goog.object.forEach obj (fn [v k] (-assoc! native (keyword k) v)))
374 | native)
375 | (seqable? obj) (let [native (native false)]
376 | (doseq [key (keys obj)]
377 | (-assoc! native (keyword key) (get obj (name key)))))
378 | :default (throw js/Error (str "Trying to convert unknown object type" (type obj)))))
379 |
380 | (defn read-only!
381 | [native]
382 | {:pre [(native? native)]}
383 | (set! (.-__ro native) true)
384 | native)
385 |
386 | (defn read-only?
387 | [native]
388 | {:pre [(native? native)]}
389 | (-read-only? native))
390 |
391 | (defn writeable!
392 | [native]
393 | {:pre [(native? native)]}
394 | (set! (.-__ro native) false)
395 | native)
396 |
397 |
398 |
399 | ;;
400 | ;; Native object store
401 | ;; ============================
402 |
403 | (defn- upsert-merge
404 | "Only called from internal methods"
405 | ([o1 o2]
406 | ;; allow updates
407 | (binding [*transaction* (if *transaction* *transaction* true)]
408 | (doseq [k (keys o2)]
409 | (let [kstr (name k)]
410 | (if-not (nil? (aget o2 kstr))
411 | (-assoc! o1 k (aget o2 kstr))
412 | (-dissoc! o1 k)))))
413 | o1)
414 | ([o1 o2 & more]
415 | (apply upsert-merge (upsert-merge o1 o2) more)))
416 |
417 | ;; Return a cursor for walking a range of the index
418 | (deftype Cursor [idx start end ^:mutable valid? empty?]
419 | IReduce
420 | (-reduce [this f]
421 | (-reduce this f (f)))
422 | (-reduce [this f init]
423 | (if empty?
424 | init
425 | (let [a (or (.-arry idx) (aget idx "arry"))]
426 | (loop [i start ret init]
427 | (if (<= i end)
428 | (recur (inc i) (f ret (aget a i)))
429 | ret)))))
430 |
431 | ISeqable
432 | (-seq [this]
433 | (seq (into [] this))))
434 |
435 |
436 | ;(deftype WrappedCursor [idx start end ^:mutable valid?]
437 | ; IReduce
438 | ; (-reduce [this f]
439 | ; (-reduce this f (f)))
440 | ; (-reduce [this f init]
441 | ; (let [a (.-arry idx)]
442 | ; (loop [i start ret init]
443 | ; (if (<= i end)
444 | ; (recur (inc i) (f ret (aget a i)))
445 | ; ret)))))
446 |
447 | ;; Hash KV Index, meant to be for a root store index (unique keys)
448 | ;; - Merging upsert against existing if keyfn output matches
449 | ;; - Nil values in provided object deletes keys
450 | ;; - Original object maintains identity
451 | (deftype HashIndex [keyfn hashmap]
452 | ILookup
453 | (-lookup [idx val]
454 | (-lookup idx val nil))
455 | (-lookup [idx val not-found]
456 | (js-lookup hashmap val not-found))
457 |
458 | IFn
459 | (-invoke [idx k]
460 | (-lookup idx k))
461 | (-invoke [idx k not-found]
462 | (-lookup idx k not-found))
463 |
464 | ICounted
465 | (-count [idx] (alength (js-keys hashmap)))
466 |
467 | IIndex
468 | (key-fn [idx] keyfn)
469 | (index! [idx obj]
470 | (let [key (keyfn obj)
471 | old (js-lookup hashmap key)]
472 | (js-assoc hashmap key (if old (upsert-merge old obj) obj))))
473 | (unindex! [idx obj]
474 | (let [key (keyfn obj)]
475 | (js-dissoc hashmap key obj)))
476 |
477 | IClearable
478 | (clear! [idx]
479 | (goog.array.clear hashmap))
480 |
481 | IScannable
482 | (-get-cursor [idx]
483 | (let [vals (js-obj "arry" (goog.object.getValues (.-hashmap idx)))]
484 | (Cursor. vals 0 (dec (alength (aget vals "arry"))) true false)))
485 | (-get-cursor [idx start]
486 | (assert false "Hash index does not support range queries"))
487 | (-get-cursor [idx start end]
488 | (assert false "Hash index does not support range queries")))
489 |
490 | (defn root-index []
491 | (HashIndex. #(aget % "id") #js {}))
492 |
493 | ;; KV index using binary search/insert/remove on array
494 | ;; - Always inserts new objects in sorted order
495 | ;; - Matches on object identity for unindex!
496 | (deftype BinaryIndex [keyfn compfn arry]
497 | ILookup
498 | (-lookup [idx val]
499 | (-lookup idx val nil))
500 | (-lookup [idx val not-found]
501 | (let [index (goog.array.binarySearch arry val #(compfn %1 (keyfn %2)))]
502 | (if (>= index 0)
503 | (loop [end index]
504 | (if (= (compfn val (keyfn (aget arry end))) 0)
505 | (recur (inc end))
506 | (goog.array.slice arry index end)))
507 | not-found)))
508 |
509 | IFn
510 | (-invoke [idx k]
511 | (-lookup idx k))
512 | (-invoke [idx k not-found]
513 | (-lookup idx k not-found))
514 |
515 | IIndex
516 | (key-fn [idx] keyfn)
517 | (index! [idx obj]
518 | (let [loc (goog.array.binarySearch arry obj #(compfn (keyfn %1) (keyfn %2)))]
519 | (if (>= loc 0)
520 | (goog.array.insertAt arry obj loc)
521 | (goog.array.insertAt arry obj (- (inc loc)))))
522 | idx)
523 | (unindex! [idx obj]
524 | (let [loc (goog.array.indexOf arry obj)]
525 | (when (>= loc 0)
526 | (goog.array.removeAt arry loc)))
527 | idx)
528 |
529 | IClearable
530 | (clear! [idx]
531 | (goog.array.clear arry))
532 |
533 | ISortedIndex
534 | (comparator-fn [idx] compfn)
535 |
536 | IScannable
537 | (-get-cursor [idx]
538 | (Cursor. idx 0 (dec (alength (.-arry idx))) true false))
539 | (-get-cursor [idx start]
540 | (let [head (goog.array.binarySearch arry start #(compfn %1 (keyfn %2)))
541 | head (if (>= head 0) head (- (inc head)))]
542 | (Cursor. idx head (dec (alength (.-arry idx))) true false)))
543 | (-get-cursor [idx start end]
544 | (let [headidx (goog.array.binarySearch arry start #(compfn %1 (keyfn %2)))
545 | head (if (>= headidx 0) headidx (- (inc headidx)))
546 | tailidx (goog.array.binarySearch arry end #(compfn %1 (keyfn %2)))
547 | tail (if (>= tailidx 0) tailidx (- (inc tailidx)))
548 | tail (if (not (>= tail (alength (.-arry idx))))
549 | (loop [tail tail]
550 | (let [next (keyfn (aget arry tail))
551 | c (compfn end next)]
552 | (if (= c 0)
553 | (if (not= (inc tail) (alength (.-arry idx)))
554 | (recur (inc tail))
555 | tail)
556 | (dec tail))))
557 | tail)]
558 | (let [empty? (and (= head tail) (and (< tailidx 0) (< headidx 0)))]
559 | (Cursor. idx head tail true empty?)))))
560 |
561 | (defn ordered-index [keyfn compfn]
562 | (BinaryIndex. keyfn compfn (array)))
563 |
564 | ;; (deftype MultiIndex [keyfn bidx]
565 | ;; IIndex
566 | ;; (key-fn [idx] keyfn)
567 | ;; (index! [idx obj]
568 | ;; (let [vals (keyfn obj)
569 | ;; len (alength vals)]
570 | ;; (loop [i (alength vals)]
571 | ;; (when (< i len)
572 | ;; (index! bidx (array (aget vals i) obj))
573 | ;; (recur (inc i))))))
574 | ;; (unindex! [idx obj]
575 | ;; (let [arry (aget bidx "arry")
576 | ;; find (fn [obj] (goog.array.findIndex arry #(= (aget % 1) obj)))]
577 | ;; (loop [loc (find obj)]
578 | ;; (when (>= loc 0)
579 | ;; (goog.array.removeAt arry loc)
580 | ;; (recur (find obj))))))
581 |
582 | ;; ISortedIndex
583 | ;; (comparator-fn [idx]
584 | ;; (comparator-fn bidx))
585 |
586 | ;; IScannable
587 | ;; (-get-cursor [idx]
588 | ;; (let [cur (-get-cursor bidx)]
589 | ;; (WrappedCursor. (aget cur "idx")
590 | ;; (aget cur "start")
591 | ;; (aget cur "end")
592 | ;; #(aget % 1)
593 | ;; (aget cur "valid?"))))
594 |
595 | ;; (-get-cursor [idx start]
596 | ;; (let [cur (-get-cursor bidx start)]
597 | ;; (WrappedCursor. (aget cur "idx")
598 | ;; (aget cur "start")
599 | ;; (aget cur "end")
600 | ;; #(aget % 1)
601 | ;; (aget cur "valid?"))))
602 |
603 | ;; (-get-cursor [idx start end]
604 | ;; (let [cur (-get-cursor bidx start end)]
605 | ;; (WrappedCursor. (aget cur "idx")
606 | ;; (aget cur "start")
607 | ;; (aget cur "end")
608 | ;; #(aget % 1)
609 | ;; (aget cur "valid?")))))
610 |
611 | ;; (defn multi-index [keyfn compfn]
612 | ;; (MultiIndex. keyfn (ordered-index #(aget val 0) compfn)))
613 |
614 | (defn compound-key-fn
615 | "Return a js array key for compound ordering"
616 | [keyfns]
617 | (let [cnt (count keyfns)]
618 | (fn [obj]
619 | (let [vals (new js/Array cnt)]
620 | (loop [i 0 keyfns keyfns]
621 | (if (empty? keyfns)
622 | vals
623 | (if-let [val ((first keyfns) obj)]
624 | (do (aset vals i val)
625 | (recur (inc i) (rest keyfns)))
626 | nil)))))))
627 |
628 | (defn compound-comparator
629 | "Compare two compound keys using the array of comparators"
630 | [comps]
631 | (let [cnt (count comps)]
632 | (fn [akey1 akey2]
633 | (loop [i 0 comps comps ans 0]
634 | (if-not (empty? comps)
635 | (let [comp (first comps)
636 | res (comp (aget akey1 i) (aget akey2 i))]
637 | (if (= res 0)
638 | (recur (inc i) (rest comps) res)
639 | res))
640 | ans)))))
641 |
642 | (defn compound-index [keyfns compfns]
643 | (BinaryIndex. (compound-key-fn keyfns) (compound-comparator compfns) (array)))
644 |
645 | (defn- as-ro-native
646 | "Ensure submitted object is a native and set to read-only state"
647 | [obj]
648 | (if (native? obj)
649 | (read-only! obj)
650 | (let [native (to-native obj)]
651 | (read-only! native))))
652 |
653 | (defn- update-listeners
654 | "Use this to update store listeners when write dependencies
655 | have been accumulatd"
656 | [result dmap]
657 | #_(.log js/console "Notifying listeners" dmap)
658 | (let [[store deps] (first dmap)]
659 | #_(.log js/console " Notifying store" store deps)
660 | (when store
661 | (d/notify-listeners store deps))))
662 |
663 | ;; A native, indexed, mutable/transactional store
664 | ;; - Always performs a merging upsert
665 | ;; - Secondary index doesn't index objects for key-fn -> nil
666 | (deftype NativeStore [root indices tx-listeners ^:mutable listeners]
667 | IPrintWithWriter
668 | (-pr-writer [native writer opts]
669 | (-write writer (str "#NativeStore[]")))
670 |
671 | ILookup
672 | (-lookup [store id]
673 | (-lookup store id nil))
674 | (-lookup [store id not-found]
675 | (inform-tracker store (js-obj "root" (array id)))
676 | (-lookup root id not-found))
677 |
678 | ICounted
679 | (-count [store] (-count root))
680 |
681 | IFn
682 | (-invoke [store k]
683 | (-lookup store k nil))
684 | (-invoke [store k not-found]
685 | (-lookup store k not-found))
686 |
687 | IStore
688 | (insert! [store obj]
689 | ;; 1) Transactional by default or participates in wrapping transaction
690 | ;; Transaction listeners get a log of all side effects per transaction
691 | ;;
692 | ;; 2) Track side effects against indices, etc and forward to enclosing
693 | ;; transaction if present or notify active dependency listeners
694 | #_(.log js/console "Called insert!\n")
695 | ;; reuse this for writes instead of reads
696 | (with-tracked-dependencies [update-listeners]
697 | (let [obj (as-ro-native obj)]
698 | (let [key ((key-fn root) obj)
699 | _ (assert key "Must have an ID field")
700 | names (js-keys indices)
701 | old (get root key)
702 | oldref (when old (js-copy old))]
703 | ;; Unindex
704 | (when old
705 | (doseq [iname names]
706 | (let [idx (aget indices iname)
707 | ikey ((key-fn idx) old)]
708 | (when-not (or (nil? ikey) (= ikey false))
709 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
710 | (unindex! idx old)))))
711 | ;; Merge-update the root
712 | #_(println "Informing tracker of root: " (js-obj "root" (array key)) "\n")
713 | (inform-tracker store (js-obj "root" (array key)))
714 | (index! root obj) ;; merging upsert
715 | (let [new (get root key)]
716 | ;; Re-insert
717 | (doseq [iname names]
718 | (let [idx (aget indices iname)
719 | ikey ((key-fn idx) new)]
720 | (when-not (or (nil? ikey) (= ikey false))
721 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
722 | (index! idx new))))
723 | ;; Update listeners
724 | (if *transaction*
725 | (.push *transaction* #js [:insert oldref new])
726 | (-notify-watches store nil #js [#js [:insert oldref new]]))))
727 | store)))
728 |
729 | (delete! [store id]
730 | (with-tracked-dependencies [update-listeners]
731 | (when-let [old (get root id)]
732 | (doseq [iname (js-keys indices)]
733 | (let [idx (aget indices iname)
734 | ikey ((key-fn idx) old)]
735 | (when-not (or (nil? ikey) (= ikey false))
736 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
737 | (unindex! idx old))))
738 | (inform-tracker store (js-obj "root" (array id)))
739 | (unindex! root old)
740 | (if *transaction*
741 | (.push *transaction* #js [:delete old])
742 | (-notify-watches store nil #js [:delete old])))
743 | store))
744 |
745 | IClearable
746 | (clear! [store]
747 | ;; Invalidate all listeners
748 | (d/force-invalidation store)
749 | ;; Cleanly remove data by reverse-walking the root
750 | (doseq [obj (seq (-get-cursor root))]
751 | (delete! store ((key-fn root) obj)))
752 | ;; Ensure we've cleared everything (e.g. dependency ordering problems)
753 | (doseq [iname (js-keys indices)]
754 | (clear! (aget indices iname)))
755 | (clear! root)
756 | store)
757 |
758 | IIndexedStore
759 | (add-index! [store iname index]
760 | (assert (not (get-index store iname)))
761 | (js-assoc indices iname index)
762 | store)
763 | (rem-index! [store iname]
764 | (assert (get-index store iname))
765 | (js-dissoc indices iname)
766 | store)
767 | (get-index [store iname]
768 | (if (or (string? iname) (keyword? iname))
769 | (js-lookup indices (name iname))
770 | iname))
771 |
772 | ITransactionalStore
773 | (-transact! [store f args]
774 | ;; TODO: separate process so we ignore read deps in transactions?
775 | (with-tracked-dependencies [update-listeners true]
776 | (binding [*transaction* #js []]
777 | (let [result (apply f store args)]
778 | (-notify-watches store nil *transaction*)))))
779 |
780 | IWatchable
781 | (-notify-watches [store _ txs]
782 | (doseq [name (js-keys tx-listeners)]
783 | (let [listener (aget tx-listeners name)]
784 | (listener nil txs)))
785 | store)
786 | (-add-watch [store key f]
787 | (js-assoc tx-listeners key f)
788 | store)
789 | (-remove-watch [store key]
790 | (js-dissoc tx-listeners key)
791 | store)
792 |
793 | d/IDependencySource
794 | (subscribe! [this listener]
795 | (set! listeners (update-in listeners [nil] (fnil conj #{}) listener)))
796 | (subscribe! [this listener deps]
797 | (set! listeners (update-in listeners [deps] (fnil conj #{}) listener)))
798 | (unsubscribe! [this listener]
799 | (let [old-set (get listeners nil)
800 | new-set (disj listeners listener)]
801 | (if (empty? new-set)
802 | (set! listeners (dissoc listeners nil))
803 | (set! listeners (assoc listeners nil new-set)))))
804 | (unsubscribe! [this listener deps]
805 | (let [old-set (get listeners deps)
806 | new-set (when (set? old-set) (disj old-set listener))]
807 | (if (empty? new-set)
808 | (set! listeners (dissoc listeners deps))
809 | (set! listeners (assoc listeners deps new-set)))))
810 | (empty-deps [this] (make-dependencies this)))
811 |
812 |
813 | (defn transact! [store f & args]
814 | (-transact! store f args))
815 |
816 | (defn create []
817 | (NativeStore. (root-index) #js {} #js {} {}))
818 |
819 |
820 | ;;
821 | ;; External interface
822 | ;;
823 |
824 | (defn fetch
825 | ([store key]
826 | (get store key))
827 | ([store index key]
828 | (-> (get-index store index) (get key))))
829 |
830 | (defn- first-val [idx]
831 | ((key-fn idx) (aget (.-arry idx) 0)))
832 |
833 | (defn- last-val [idx]
834 | ((key-fn idx) (aget (.-arry idx) (- (alength (.-arry idx)) 1))))
835 |
836 | (defn cursor
837 | "Walk the entire store, or an index"
838 | ([store]
839 | ;; NOTE: No dependencies on whole DB
840 | (-get-cursor (.-root store)))
841 | ([store index]
842 | (let [iname (name index)
843 | index (get-index store iname)]
844 | (assert index (str "Index " iname " is not defined."))
845 | (inform-tracker store (js-obj iname (array)))
846 | (-get-cursor index)))
847 | ([store index start]
848 | (let [iname (name index)
849 | index (get-index store index)]
850 | (assert index (str "Index " iname " is not defined."))
851 | (inform-tracker store (js-obj iname (array start))) ;; shorthand
852 | (-get-cursor index start)))
853 | ([store index start end]
854 | (let [iname (name index)
855 | index (get-index store index)]
856 | (assert index (str "Index " iname " is not defined."))
857 | (inform-tracker store (js-obj iname (array start end)))
858 | (-get-cursor index start end))))
859 |
860 | (defn field-key [field]
861 | (let [f (name field)]
862 | (fn [obj]
863 | (aget obj f))))
864 |
865 | (defn type-field-key [type field]
866 | (let [t (name type)
867 | f (name field)]
868 | (fn [obj]
869 | (when (= t (aget obj "type"))
870 | (aget obj f)))))
871 |
872 | (defn ensure-index
873 | ([store iname key comp]
874 | (when-not (get-index store iname)
875 | (add-index! store iname (ordered-index (if (fn? key) key (field-key key)) comp))))
876 | ([store iname key-or-idx]
877 | (if (or (keyword? key-or-idx) (symbol? key-or-idx))
878 | (ensure-index store iname key-or-idx compare)
879 | (when-not (get-index store iname)
880 | (add-index! store iname key-or-idx))))) ;; key is an index
881 |
882 | (comment
883 | (def store (native-store))
884 | (add-index! store :name (ordered-index (field-key :name) compare))
885 | (insert! store #js {:id 1 :type "user" :name "Fred"})
886 | (insert! store #js {:id 2 :type "user" :name "Zoe"})
887 | (insert! store #js {:id 3 :type "user" :name "Apple"})
888 | (insert! store #js {:id 4 :type "user" :name "Flora"})
889 | (insert! store #js {:id 5 :type "user" :name "Flora"})
890 | (insert! store #js {:id 6 :type "tracker" :measure 2700})
891 | (println "Get by ID" (get store 1))
892 | (println "Get by index" (-> (get-index store :name) (get "Flora")))
893 | (println (js->clj (r/reduce (cursor store :name) (d/map :id))))) ;; object #6 is not indexed!
894 |
895 |
896 |
897 |
--------------------------------------------------------------------------------
/src/nativestore/heap.cljs:
--------------------------------------------------------------------------------
1 | (ns nativestore.heap
2 | (:require [goog.object :as gobj]
3 | [goog.array :as garr]
4 | [derive.core :as d :refer-macros [with-tracked-dependencies defnd]]))
5 |
6 | ;;
7 | ;; Legacy JS wrappers
8 | ;;
9 |
10 | (defn js-strkey [x]
11 | (cond
12 | (string? x) x
13 | (keyword? x) (name x)
14 | :else (str x)))
15 |
16 | (defn js-lookup
17 | ([o k]
18 | (aget o (js-strkey k)))
19 | ([o k not-found]
20 | (let [s (js-strkey k)]
21 | (if-let [res (aget o s)]
22 | res
23 | not-found))))
24 |
25 | (defn js-copy
26 | [o]
27 | (let [t (js/goog.typeOf o)]
28 | (cond (= t "array") (garr/clone o)
29 | :else (gobj/clone o))))
30 |
31 | (defn js-assoc
32 | ([o k v]
33 | (do (aset o (js-strkey k) v)
34 | o))
35 | ([o k v & more]
36 | (js-assoc o k v)
37 | (if more
38 | (recur o (first more) (second more) (nnext more))
39 | o)))
40 |
41 | (defn js-dissoc
42 | [o k & more]
43 | (js-delete o (js-strkey k))
44 | (if more
45 | (recur o (first more) (next more))
46 | o))
47 |
48 | ;;
49 | ;; Store protocols
50 | ;; ============================
51 |
52 | ;; + ILookup
53 | (defprotocol IStore
54 | (insert! [store obj]) ;; shallow merge upsert of native objects
55 | (delete! [store id])) ;; delete, only need primary ID in submitted object
56 |
57 | ;; CompFn(KeyFn(obj)) -> value, obj
58 | (defprotocol IIndex
59 | (key-fn [idx])
60 | (index! [idx obj])
61 | (unindex! [idx obj]))
62 |
63 | (defprotocol IClearable
64 | (clear! [idx]))
65 |
66 | (defprotocol ISortedIndex
67 | (comparator-fn [idx]))
68 |
69 | (defprotocol IScannable
70 | (-get-cursor [idx] [idx start] [idx start end]))
71 |
72 | (defprotocol IIndexedStore
73 | (add-index! [store name index])
74 | (rem-index! [store name])
75 | (get-index [store name]))
76 |
77 | (defprotocol ITransactionalStore
78 | (-transact! [store f args]))
79 |
80 | ;;
81 | ;; Native Dependencies
82 | ;; ============================
83 |
84 | ;; A dependency representation is:
85 | ;;
86 | ;; #js { root: #js [ ...]
87 | ;; : #js [start end] }
88 | ;;
89 | ;; The root is a sorted list of object IDs (reference traversal or direct lookups)
90 | ;; The remaining index lookups maintain value ranges traversed
91 | ;; These become set intersection and range overlap calculations when testing
92 | ;; for the impact of a transaction
93 | ;;
94 | ;; The left side dependency is mutated and returned by all operations
95 | ;;
96 |
97 | (defn- sorted-insert!
98 | "Mutates r1. Keep list of merged IDs in sorted order"
99 | [r1 r2]
100 | (goog.array.forEach r2 (fn [v i a] (goog.array.binaryInsert r1 v))))
101 |
102 | (defn- merge-range!
103 | "Mutates r1. The updated range becomes the union of the two ranges"
104 | [compfn range1 range2]
105 | (let [r1s (aget range1 0)
106 | r1e (aget range1 1)
107 | r2s (aget range2 0)
108 | r2e (aget range2 1)]
109 | (when (< (compfn r2s r1s) 0)
110 | (aset range1 0 r2s))
111 | (when (> (compfn r2e r1e) 0)
112 | (aset range1 1 r2e))))
113 |
114 | (defn- merge-index!
115 | "Merge the index range or root set"
116 | [nset idx range1 range2]
117 | #_(println "idx: " (type idx) "r1: " range1 "r2: " range2 "\n")
118 | (if (nil? idx) ; root?
119 | (sorted-insert! range1 range2)
120 | (merge-range! (comparator-fn idx) range1 range2)))
121 |
122 | (defn- intersect?
123 | "Do two sorted sets of integers intersect?"
124 | [set1 set2]
125 | #_(println "Intersect? " set1 set2)
126 | (let [len1 (if (nil? set1) 0 (alength set1))
127 | len2 (if (nil? set2) 0 (alength set2))]
128 | (loop [i 0 j 0]
129 | (if (or (== i len1) (== j len2))
130 | false
131 | (let [v1 (aget set1 i)
132 | v2 (aget set2 j)]
133 | (cond (= v1 v2) true
134 | (> (compare v1 v2) 0) (recur i (inc j))
135 | :default (recur (inc i) j)))))))
136 |
137 | ;; (0 10) (2 2) => true
138 | ;; (10 20) (5 5) => false
139 | ;; (10 20) (0 10) => true
140 | ;; (10 20) (0 nil) => true
141 | ;; (10 nil) (20 20) => true
142 | (defn- overlap?
143 | "Does the interval of other overlap this?"
144 | [compfn range1 range2]
145 | (let [r1s (aget range1 0)
146 | r1e (aget range1 1)
147 | r2s (aget range2 0)
148 | r2e (aget range2 1)
149 | res (not (or (if (nil? r1e) (< (compfn r2e r1s) 0) (> (compfn r2s r1e) 0))
150 | (if (nil? r2e) (< (compfn r1e r2s) 0) (< (compfn r2e r1s) 0))))]
151 | #_(println "Overlap? " range1 range2 res)
152 | res))
153 |
154 |
155 | (defn- match-index?
156 | [nset idx this-range other-range]
157 | #_(println "Matching index: " this-range " " other-range "\n")
158 | (if (nil? idx) ; root?
159 | (intersect? this-range other-range)
160 | (overlap? (comparator-fn idx) this-range other-range)))
161 |
162 | (deftype NativeDependencySet [store deps]
163 | IPrintWithWriter
164 | (-pr-writer [native writer opts]
165 | (-write writer (str "#ndep [" (pr-str deps) "]")))
166 |
167 | IHash
168 | (-hash [o]
169 | (goog/getUid o))
170 |
171 | IEquiv
172 | (-equiv [o other]
173 | (if (instance? NativeDependencySet other)
174 | (== (-hash o) (-hash other))
175 | false))
176 |
177 | d/IDependencySet
178 | (merge-deps [nset other]
179 | #_(println "NSet merge: " (type store) deps other "\n")
180 | (let [fdeps (if (nil? (.-deps other)) other (.-deps other))]
181 | (goog.object.forEach
182 | fdeps (fn [v k]
183 | (if-let [mine (aget deps k)]
184 | (merge-index! nset (get-index store k) mine v)
185 | (aset deps k (js-copy v)))))
186 | nset))
187 |
188 | (match-deps [nset other]
189 | (let [fdeps (if (nil? (.-deps other)) other (.-deps other))]
190 | #_(println "Matching: " deps fdeps "\n")
191 | (goog.object.some
192 | fdeps (fn [v k o] #_(println "matching-key: " k "\n")
193 | (when-let [local (aget deps k)]
194 | (match-index? nset (get-index store k) local v)))))))
195 |
196 |
197 | (defn make-dependencies
198 | ([store] (NativeDependencySet. store #js {}))
199 | ([store init] (NativeDependencySet. store init)))
200 |
201 | (defn inform-tracker
202 | ([store deps]
203 | (when (d/tracking?)
204 | (inform-tracker d/*tracker* store deps)))
205 | ([tracker store deps]
206 | (let [dset (make-dependencies store deps)]
207 | (.log js/console "Informing tracker: " dset " t? " d/*tracker* "\n")
208 | (d/depends! tracker store dset))))
209 |
210 | ;;
211 | ;; Instance protocols
212 | ;; ============================
213 |
214 | (def ^{:doc "Inside a transaction?"
215 | :dynamic true}
216 | *transaction* nil)
217 |
218 | ;; A reference wraps a lookup into a store
219 | ;; Objects implementing ILookup can test for a
220 | ;; IReference and dereference it.
221 |
222 | (defprotocol IReference
223 | (resolve-ref [ref])
224 | (reference-id [ref])
225 | (reference-db [ref]))
226 |
227 | (deftype HeapReference [store id]
228 | IDeref
229 | (-deref [ref]
230 | (resolve-ref ref))
231 |
232 | IPrintWithWriter
233 | (-pr-writer [native writer opts]
234 | (-write writer (str "#ref [" id "]")))
235 |
236 | IEquiv
237 | (-equiv [ref other]
238 | (and (= store (.-store other))
239 | (= id (.-id other))))
240 |
241 | IReference
242 | (resolve-ref [_] (get store id))
243 | (reference-id [_] id)
244 | (reference-db [_] store))
245 |
246 | (declare heap-map? heap-map-store heap-id -store)
247 |
248 | (defn reference
249 | "Return a reference to value in store, if using the 1-arity version
250 | the argument must be a heap map"
251 | ([value]
252 | (assert (heap-map? value))
253 | (HeapReference. (-store value) (-wrapped-value value)))
254 | ([store value]
255 | (HeapReference.
256 | store
257 | (if (map? value)
258 | (heap-id store value)
259 | value))))
260 |
261 | (defn identity? [n1 n2]
262 | (= (aget n1 "id") (aget n2 "id")))
263 |
264 |
265 | ;;
266 | ;; Heap Store
267 | ;; ============================
268 |
269 | ;; Return a cursor for walking a range of the index
270 | (deftype Cursor [idx start end ^:mutable valid? empty?]
271 | IReduce
272 | (-reduce [this f]
273 | (-reduce this f (f)))
274 | (-reduce [this f init]
275 | (if empty?
276 | init
277 | (let [a (or (.-arry idx) (aget idx "arry"))]
278 | (loop [i start ret init]
279 | (if (<= i end)
280 | (recur (inc i) (f ret (aget a i)))
281 | ret)))))
282 | ISeqable
283 | (-seq [this]
284 | (seq (into [] this))))
285 |
286 |
287 | ;(deftype WrappedCursor [idx start end ^:mutable valid?]
288 | ; IReduce
289 | ; (-reduce [this f]
290 | ; (-reduce this f (f)))
291 | ; (-reduce [this f init]
292 | ; (let [a (.-arry idx)]
293 | ; (loop [i start ret init]
294 | ; (if (<= i end)
295 | ; (recur (inc i) (f ret (aget a i)))
296 | ; ret)))))
297 |
298 | ;; Hash KV Index, meant to be for a root store index (unique keys)
299 | ;; - Merging upsert against existing if keyfn output matches
300 | ;; - Nil values in provided object deletes keys
301 | ;; - Original object maintains identity
302 | (deftype HashIndex [keyfn hashmap]
303 | ILookup
304 | (-lookup [idx val]
305 | (-lookup idx val nil))
306 | (-lookup [idx val not-found]
307 | (js-lookup hashmap val not-found))
308 |
309 | IFn
310 | (-invoke [idx k]
311 | (-lookup idx k))
312 | (-invoke [idx k not-found]
313 | (-lookup idx k not-found))
314 |
315 | ICounted
316 | (-count [idx] (alength (js-keys hashmap)))
317 |
318 | IIndex
319 | (key-fn [idx] keyfn)
320 | (index! [idx obj]
321 | (let [key (keyfn obj)]
322 | (js-assoc hashmap key obj)))
323 | (unindex! [idx obj]
324 | (let [key (keyfn obj)]
325 | (js-dissoc hashmap key obj)))
326 |
327 | IClearable
328 | (clear! [idx]
329 | (goog.array.clear hashmap))
330 |
331 | IScannable
332 | (-get-cursor [idx]
333 | (let [vals (js-obj "arry" (goog.object.getValues (.-hashmap idx)))]
334 | (Cursor. vals 0 (dec (alength (aget vals "arry"))) true false)))
335 | (-get-cursor [idx start]
336 | (assert false "Hash index does not support range queries"))
337 | (-get-cursor [idx start end]
338 | (assert false "Hash index does not support range queries")))
339 |
340 | (defn root-index []
341 | (HashIndex. :id #js {}))
342 |
343 | ;; KV index using binary search/insert/remove on array
344 | ;; - Always inserts new objects in sorted order
345 | ;; - Matches on object identity for unindex!
346 | (deftype BinaryIndex [keyfn compfn arry]
347 | ILookup
348 | (-lookup [idx val]
349 | (-lookup idx val nil))
350 | (-lookup [idx val not-found]
351 | (let [index (goog.array.binarySearch arry val #(compfn %1 (keyfn %2)))]
352 | (if (>= index 0)
353 | (loop [end index]
354 | (if (= (compfn val (keyfn (aget arry end))) 0)
355 | (recur (inc end))
356 | (goog.array.slice arry index end)))
357 | not-found)))
358 |
359 | IFn
360 | (-invoke [idx k]
361 | (-lookup idx k))
362 | (-invoke [idx k not-found]
363 | (-lookup idx k not-found))
364 |
365 | IIndex
366 | (key-fn [idx] keyfn)
367 | (index! [idx obj]
368 | (let [loc (goog.array.binarySearch arry obj #(compfn (keyfn %1) (keyfn %2)))]
369 | (if (>= loc 0)
370 | (goog.array.insertAt arry obj loc)
371 | (goog.array.insertAt arry obj (- (inc loc)))))
372 | idx)
373 | (unindex! [idx obj]
374 | (let [loc (goog.array.indexOf arry obj)]
375 | (when (>= loc 0)
376 | (goog.array.removeAt arry loc)))
377 | idx)
378 |
379 | IClearable
380 | (clear! [idx]
381 | (goog.array.clear arry))
382 |
383 | ISortedIndex
384 | (comparator-fn [idx] compfn)
385 |
386 | IScannable
387 | (-get-cursor [idx]
388 | (Cursor. idx 0 (dec (alength (.-arry idx))) true false))
389 | (-get-cursor [idx start]
390 | (let [head (goog.array.binarySearch arry start #(compfn %1 (keyfn %2)))
391 | head (if (>= head 0) head (- (inc head)))]
392 | (Cursor. idx head (dec (alength (.-arry idx))) true false)))
393 | (-get-cursor [idx start end]
394 | (let [headidx (goog.array.binarySearch arry start #(compfn %1 (keyfn %2)))
395 | head (if (>= headidx 0) headidx (- (inc headidx)))
396 | tailidx (goog.array.binarySearch arry end #(compfn %1 (keyfn %2)))
397 | tail (if (>= tailidx 0) tailidx (- (inc tailidx)))
398 | tail (if (not (>= tail (alength (.-arry idx))))
399 | (loop [tail tail]
400 | (let [next (keyfn (aget arry tail))
401 | c (compfn end next)]
402 | (if (= c 0)
403 | (if (not= (inc tail) (alength (.-arry idx)))
404 | (recur (inc tail))
405 | tail)
406 | (dec tail))))
407 | tail)]
408 | (let [empty? (and (= head tail) (and (< tailidx 0) (< headidx 0)))]
409 | (Cursor. idx head tail true empty?)))))
410 |
411 | (defn ordered-index [keyfn compfn]
412 | (BinaryIndex. keyfn compfn (array)))
413 |
414 | ;; (deftype MultiIndex [keyfn bidx]
415 | ;; IIndex
416 | ;; (key-fn [idx] keyfn)
417 | ;; (index! [idx obj]
418 | ;; (let [vals (keyfn obj)
419 | ;; len (alength vals)]
420 | ;; (loop [i (alength vals)]
421 | ;; (when (< i len)
422 | ;; (index! bidx (array (aget vals i) obj))
423 | ;; (recur (inc i))))))
424 | ;; (unindex! [idx obj]
425 | ;; (let [arry (aget bidx "arry")
426 | ;; find (fn [obj] (goog.array.findIndex arry #(= (aget % 1) obj)))]
427 | ;; (loop [loc (find obj)]
428 | ;; (when (>= loc 0)
429 | ;; (goog.array.removeAt arry loc)
430 | ;; (recur (find obj))))))
431 |
432 | ;; ISortedIndex
433 | ;; (comparator-fn [idx]
434 | ;; (comparator-fn bidx))
435 |
436 | ;; IScannable
437 | ;; (-get-cursor [idx]
438 | ;; (let [cur (-get-cursor bidx)]
439 | ;; (WrappedCursor. (aget cur "idx")
440 | ;; (aget cur "start")
441 | ;; (aget cur "end")
442 | ;; #(aget % 1)
443 | ;; (aget cur "valid?"))))
444 |
445 | ;; (-get-cursor [idx start]
446 | ;; (let [cur (-get-cursor bidx start)]
447 | ;; (WrappedCursor. (aget cur "idx")
448 | ;; (aget cur "start")
449 | ;; (aget cur "end")
450 | ;; #(aget % 1)
451 | ;; (aget cur "valid?"))))
452 |
453 | ;; (-get-cursor [idx start end]
454 | ;; (let [cur (-get-cursor bidx start end)]
455 | ;; (WrappedCursor. (aget cur "idx")
456 | ;; (aget cur "start")
457 | ;; (aget cur "end")
458 | ;; #(aget % 1)
459 | ;; (aget cur "valid?")))))
460 |
461 | ;; (defn multi-index [keyfn compfn]
462 | ;; (MultiIndex. keyfn (ordered-index #(aget val 0) compfn)))
463 |
464 | (defn compound-key-fn
465 | "Return a js array key for compound ordering"
466 | [keyfns]
467 | (let [cnt (count keyfns)]
468 | (fn [obj]
469 | (let [vals (new js/Array cnt)]
470 | (loop [i 0 keyfns keyfns]
471 | (if (empty? keyfns)
472 | vals
473 | (if-let [val ((first keyfns) obj)]
474 | (do (aset vals i val)
475 | (recur (inc i) (rest keyfns)))
476 | nil)))))))
477 |
478 | (defn compound-comparator
479 | "Compare two compound keys using the array of comparators"
480 | [comps]
481 | (let [cnt (count comps)]
482 | (fn [akey1 akey2]
483 | (loop [i 0 comps comps ans 0]
484 | (if-not (empty? comps)
485 | (let [comp (first comps)
486 | res (comp (aget akey1 i) (aget akey2 i))]
487 | (if (= res 0)
488 | (recur (inc i) (rest comps) res)
489 | res))
490 | ans)))))
491 |
492 | (defn compound-index [keyfns compfns]
493 | (BinaryIndex. (compound-key-fn keyfns) (compound-comparator compfns) (array)))
494 |
495 | (defn- update-listeners
496 | "Use this to update store listeners when write dependencies
497 | have been accumulatd"
498 | [result dmap]
499 | #_(.log js/console "Notifying listeners" dmap)
500 | (let [[store deps] (first dmap)]
501 | #_(.log js/console " Notifying store" store deps)
502 | (when store
503 | (d/notify-listeners store deps))))
504 |
505 | (defprotocol IHeapMap
506 | (-store [this])
507 | (-wrapped-value [this]))
508 |
509 | (defn heap-map?
510 | [value]
511 | (satisfies? value IHeapMap))
512 |
513 | (defn heap-map-store
514 | [value]
515 | (-store value))
516 |
517 | (defn wrapped-value
518 | [value]
519 | (-wrapped-value value))
520 |
521 | (declare TransientHeapMap)
522 |
523 | ;; An element stored in a heap store is wrapped in the HeapMap
524 | ;; interface which is like a clojure map in all ways except it
525 | ;; allows for graph-like traversal across references within
526 | ;; the heap while participating in the derive protocol.
527 | (deftype HeapMap [store value]
528 | IHeapMap
529 | (-store [this]
530 | store)
531 | (-wrapped-value [this]
532 | value)
533 |
534 | ILookup
535 | (-lookup [this k]
536 | (-lookup value k nil))
537 | (-lookup [this k not-found]
538 | (let [v (-lookup value k ::not-found)]
539 | (cond (= v ::not-found)
540 | not-found
541 | (sequential? v)
542 | (if (satisfies? IReference (first v))
543 | (mapv resolve-ref v)
544 | v)
545 | (satisfies? IReference v)
546 | (resolve-ref v)
547 | :else v)))
548 |
549 | ICloneable
550 | (-clone [this] (HeapMap. store (-clone value)))
551 |
552 | IWithMeta
553 | (-with-meta [this meta] (HeapMap. store (-with-meta value meta)))
554 |
555 | IMeta
556 | (-meta [this] (-meta value))
557 |
558 | ICollection
559 | (-conj [this entry] (HeapMap. store (-conj value entry)))
560 |
561 | IEmptyableCollection
562 | (-empty [this] (HeapMap. store (-empty value)))
563 |
564 | ;; Should we be equivalent with plain values? (yes for now)
565 | IEquiv
566 | (-equiv [this other]
567 | (if (heap-map? other)
568 | (and (-equiv store (-store other))
569 | (-equiv (-wrapped-value value) (-wrapped-value other)))
570 | (-equiv value other)))
571 |
572 | IHash
573 | (-hash [this] (hash value))
574 |
575 | ICounted
576 | (-count [_] (-count value))
577 |
578 | IFn
579 | (-invoke [this k]
580 | (-lookup this k))
581 | (-invoke [this k not-found]
582 | (-lookup this k not-found))
583 |
584 | ;; NOTE: Do we want to maintain the heap wrapper abstraction on sub-values?
585 | ISeqable
586 | (-seq [this]
587 | (-seq value))
588 |
589 | IAssociative
590 | (-contains-key? [_ k]
591 | (-contains-key? value k))
592 | (-assoc [_ k v]
593 | (HeapMap. store (-assoc value k v)))
594 |
595 | IEditableCollection
596 | (-as-transient [this]
597 | (TransientHeapMap. store (transient value) true))
598 |
599 | IMap
600 | (-dissoc [_ k]
601 | (HeapMap. store (-dissoc value k)))
602 |
603 | IKVReduce
604 | (-kv-reduce [_ f init]
605 | (-kv-reduce value f init))
606 |
607 | IPrintWithWriter
608 | (-pr-writer [_ writer opts]
609 | (-pr-writer value writer opts)))
610 |
611 | (defn as-heap-map
612 | [store value]
613 | (assert (map? value))
614 | (if (heap-map? value)
615 | (do (assert (identical? store (-store value)))
616 | value)
617 | (HeapMap. store value)))
618 |
619 | (deftype TransientHeapMap [store ^:mutable tvalue ^:mutable edit]
620 | IHeapMap
621 | (-store [this]
622 | store)
623 | (-wrapped-value [this]
624 | tvalue)
625 |
626 | ITransientCollection
627 | (-conj! [this elt]
628 | (if edit
629 | (set! tvalue (-conj! tvalue elt))
630 | (throw (js/Error. "conj! after persistent!")))
631 | this)
632 | (-persistent! [this]
633 | (if edit
634 | (do (set! edit nil)
635 | (HeapMap. store (-persistent! tvalue)))
636 | (throw (js/Error. "persistent! called more than once"))))
637 |
638 | ITransientAssociative
639 | (-assoc! [this k v]
640 | (if edit
641 | (set! tvalue (-assoc! tvalue k v))
642 | (throw (js/Error. "assoc! after persistent!")))
643 | this)
644 |
645 | ITransientMap
646 | (-dissoc! [this k]
647 | (if edit
648 | (set! tvalue (-dissoc! tvalue k))
649 | (throw (js/Error. "assoc! after persistent!")))
650 | this)
651 |
652 | ILookup
653 | (-lookup [value k]
654 | (-lookup value k nil))
655 | (-lookup [value k not-found]
656 | (let [v (-lookup tvalue k ::not-found)]
657 | (cond (= v ::not-found)
658 | not-found
659 | (sequential? v)
660 | (if (satisfies? IReference (first v))
661 | (mapv resolve-ref v)
662 | v)
663 | (satisfies? IReference v)
664 | (resolve-ref v)
665 | :else v)))
666 |
667 | ICounted
668 | (-count [_] (-count tvalue))
669 |
670 | IFn
671 | (-invoke [this k]
672 | (-lookup this k))
673 | (-invoke [this k not-found]
674 | (-lookup this k not-found)))
675 |
676 |
677 | ;; An Indexed Mutable Heap Store
678 | ;; - Any object added gains special keyword access supporting
679 | ;; reference resolution when accessing keyword attributes
680 | ;; - Stores any object, but generally clojurescript objects
681 | ;; - Will maintain secondary (e.g. sorted) indexes on object attributes
682 | ;; - Secondary index doesn't index objects for key-fn -> nil
683 | (deftype HeapStore [root indices tx-listeners ^:mutable listeners]
684 | IPrintWithWriter
685 | (-pr-writer [native writer opts]
686 | (-write writer (str "#HeapStore[]")))
687 |
688 | ILookup
689 | (-lookup [store id]
690 | (-lookup store id nil))
691 | (-lookup [store id not-found]
692 | (inform-tracker store (js-obj "root" (array id)))
693 | (-lookup root id not-found))
694 |
695 | ICounted
696 | (-count [store] (-count root))
697 |
698 | IFn
699 | (-invoke [store k]
700 | (-lookup store k nil))
701 | (-invoke [store k not-found]
702 | (-lookup store k not-found))
703 |
704 | IStore
705 | (insert! [store value]
706 | ;; 1) Transactional by default or participates in wrapping transaction
707 | ;; Transaction listeners get a log of all side effects per transaction
708 | ;;
709 | ;; 2) Track side effects against indices, etc and forward to enclosing
710 | ;; transaction if present or notify active dependency listeners
711 | #_(.log js/console "Called insert!\n")
712 | ;; reuse this for writes instead of reads
713 | (with-tracked-dependencies [update-listeners]
714 | (let [key ((key-fn root) value)
715 | _ (assert key "Must have an ID field")
716 | names (js-keys indices)
717 | old (get root key)]
718 | ;; Unindex
719 | (when old
720 | (doseq [iname names]
721 | (let [idx (aget indices iname)
722 | ikey ((key-fn idx) old)]
723 | (when-not (or (nil? ikey) (= ikey false))
724 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
725 | (unindex! idx old)))))
726 | ;; Update the root value at key
727 | #_(println "Informing tracker of root: " (js-obj "root" (array key)) "\n")
728 | (inform-tracker store (js-obj "root" (array key)))
729 | (index! root (as-heap-value store value))
730 | (let [new (get root key)]
731 | ;; Re-insert
732 | (doseq [iname names]
733 | (let [idx (aget indices iname)
734 | ikey ((key-fn idx) new)]
735 | (when-not (or (nil? ikey) (= ikey false))
736 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
737 | (index! idx new))))
738 | ;; Update listeners
739 | (if *transaction*
740 | (.push *transaction* #js [:insert old new])
741 | (-notify-watches store nil [[:insert old new]]))))
742 | store))
743 |
744 | (delete! [store id]
745 | (with-tracked-dependencies [update-listeners]
746 | (when-let [old (get root id)]
747 | (doseq [iname (js-keys indices)]
748 | (let [idx (aget indices iname)
749 | ikey ((key-fn idx) old)]
750 | (when-not (or (nil? ikey) (= ikey false))
751 | (inform-tracker store (js-obj (name iname) (array ikey ikey)))
752 | (unindex! idx old))))
753 | (inform-tracker store (js-obj "root" (array id)))
754 | (unindex! root old)
755 | (if *transaction*
756 | (.push *transaction* #js [:delete old])
757 | (-notify-watches store nil #js [:delete old])))
758 | store))
759 |
760 | IClearable
761 | (clear! [store]
762 | ;; Invalidate all listeners
763 | (d/force-invalidation store)
764 | ;; Cleanly remove data by reverse-walking the root
765 | (doseq [obj (seq (-get-cursor root))]
766 | (delete! store ((key-fn root) obj)))
767 | ;; Ensure we've cleared everything (e.g. dependency ordering problems)
768 | (doseq [iname (js-keys indices)]
769 | (clear! (aget indices iname)))
770 | (clear! root)
771 | store)
772 |
773 | IIndexedStore
774 | (add-index! [store iname index]
775 | (assert (not (get-index store iname)))
776 | (js-assoc indices iname index)
777 | store)
778 | (rem-index! [store iname]
779 | (assert (get-index store iname))
780 | (js-dissoc indices iname)
781 | store)
782 | (get-index [store iname]
783 | (if (or (string? iname) (keyword? iname))
784 | (js-lookup indices (name iname))
785 | iname))
786 |
787 | ITransactionalStore
788 | (-transact! [store f args]
789 | ;; TODO: separate process so we ignore read deps in transactions?
790 | (with-tracked-dependencies [update-listeners true]
791 | (binding [*transaction* #js []]
792 | (let [result (apply f store args)]
793 | (-notify-watches store nil *transaction*)))))
794 |
795 | IWatchable
796 | (-notify-watches [store _ txs]
797 | (doseq [name (js-keys tx-listeners)]
798 | (let [listener (aget tx-listeners name)]
799 | (listener nil txs)))
800 | store)
801 | (-add-watch [store key f]
802 | (js-assoc tx-listeners key f)
803 | store)
804 | (-remove-watch [store key]
805 | (js-dissoc tx-listeners key)
806 | store)
807 |
808 | d/IDependencySource
809 | (subscribe! [this listener]
810 | (set! listeners (update-in listeners [nil] (fnil conj #{}) listener))
811 | this)
812 | (subscribe! [this listener deps]
813 | (set! listeners (update-in listeners [deps] (fnil conj #{}) listener))
814 | this)
815 | (unsubscribe! [this listener]
816 | (let [old-set (get listeners nil)
817 | new-set (disj listeners listener)]
818 | (if (empty? new-set)
819 | (set! listeners (dissoc listeners nil))
820 | (set! listeners (assoc listeners nil new-set))))
821 | this)
822 | (unsubscribe! [this listener deps]
823 | (let [old-set (get listeners deps)
824 | new-set (when (set? old-set) (disj old-set listener))]
825 | (if (empty? new-set)
826 | (set! listeners (dissoc listeners deps))
827 | (set! listeners (assoc listeners deps new-set))))
828 | this)
829 | (empty-deps [this] (make-dependencies this)))
830 |
831 | (defn transact! [store f & args]
832 | (-transact! store f args))
833 |
834 | (defn create []
835 | (HeapStore. (root-index) #js {} #js {} {}))
836 |
837 | (defn heap-id
838 | ([value]
839 | (assert (heap-map? value) "Only HeapMaps have values for the 1-arity call")
840 | (heap-id (-store value) (-wrapped-value value)))
841 | ([store value]
842 | ((key-fn (.-root store)) value)))
843 | ;;
844 | ;; External interface
845 | ;;
846 |
847 | (defn fetch
848 | ([store key]
849 | (get store key))
850 | ([store index key]
851 | (-> (get-index store index) (get key))))
852 |
853 | (defn- first-val [idx]
854 | ((key-fn idx) (aget (.-arry idx) 0)))
855 |
856 | (defn- last-val [idx]
857 | ((key-fn idx) (aget (.-arry idx) (- (alength (.-arry idx)) 1))))
858 |
859 | (defn cursor
860 | "Walk the entire store, or an index"
861 | ([store]
862 | ;; NOTE: No dependencies on whole DB
863 | (-get-cursor (.-root store)))
864 | ([store index]
865 | (let [iname (name index)
866 | index (get-index store iname)]
867 | (assert index (str "Index " iname " is not defined."))
868 | (inform-tracker store (js-obj iname (array)))
869 | (-get-cursor index)))
870 | ([store index start]
871 | (let [iname (name index)
872 | index (get-index store index)]
873 | (assert index (str "Index " iname " is not defined."))
874 | (inform-tracker store (js-obj iname (array start))) ;; shorthand
875 | (-get-cursor index start)))
876 | ([store index start end]
877 | (let [iname (name index)
878 | index (get-index store index)]
879 | (assert index (str "Index " iname " is not defined."))
880 | (inform-tracker store (js-obj iname (array start end)))
881 | (-get-cursor index start end))))
882 |
883 | (defn ensure-index
884 | ([store iname key comp]
885 | (when-not (get-index store iname)
886 | (add-index! store iname (ordered-index key comp))))
887 | ([store iname key-or-idx]
888 | (if (or (keyword? key-or-idx) (symbol? key-or-idx))
889 | (ensure-index store iname key-or-idx compare)
890 | (when-not (get-index store iname)
891 | (add-index! store iname key-or-idx))))) ;; key is an index
892 |
893 | (comment
894 | (def store (create))
895 | (add-index! store :name (ordered-index (field-key :name) compare))
896 | (insert! store #js {:id 1 :type "user" :name "Fred"})
897 | (insert! store #js {:id 2 :type "user" :name "Zoe"})
898 | (insert! store #js {:id 3 :type "user" :name "Apple"})
899 | (insert! store #js {:id 4 :type "user" :name "Flora"})
900 | (insert! store #js {:id 5 :type "user" :name "Flora"})
901 | (insert! store #js {:id 6 :type "tracker" :measure 2700})
902 | (println "Get by ID" (get store 1))
903 | (println "Get by index" (-> (get-index store :name) (get "Flora")))
904 | (println (js->clj (r/reduce (cursor store :name) (d/map :id))))) ;; object #6 is not indexed!
905 |
906 |
907 |
908 |
--------------------------------------------------------------------------------