-
-
Save pithyless/c462b3c28d289921bb54 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns freactive-datascript.core | |
(:require [datascript :as d] | |
[datascript.core :as dc] | |
[clojure.data :as data] | |
[freactive.core :as f :refer [IReactive | |
*invalidate-rx* | |
*trace-capture*]])) | |
;;; Playground ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(comment | |
(declare create-conn) | |
(def conn (create-conn {})) | |
(d/transact! conn [{:db/id -1 :name "Mike" :job "Astronaut"} | |
{:db/id -2 :name "Joel" :job "Janitor"}]) | |
) | |
;;; Helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(defn ent->map [ent] (into {} ent)) | |
(defn reset-diff-tx | |
"Creates a valid datascript transaction vector where the [attr val] pairs in | |
`minus` are retracted and the ones in `plus` are added." | |
[eid minus plus] | |
(vec | |
(concat | |
(mapv (fn [[attr _]] [:db.fn/retractAttribute eid attr]) minus) | |
(mapv (fn [[attr val]] [:db/add eid attr val]) plus)))) | |
(defn entity-reset! | |
[conn eid new-value] | |
(let [entity (d/entity @conn eid) | |
ent-map (ent->map entity) | |
[minus plus _] (data/diff ent-map new-value)] | |
(d/transact! conn (reset-diff-tx eid minus plus)))) | |
;;; ReactiveEntity -- Approach 1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(deftype ReactiveDSEntity [state conn eid meta validator watches] | |
Object | |
(equiv [this other] | |
(-equiv this other)) | |
IEquiv | |
(-equiv [this other] (identical? this other)) | |
IHash | |
(-hash [this] (goog/getUid this)) | |
IMeta | |
(-meta [this] meta) | |
IWithMeta | |
(-with-meta [this meta] | |
(ReactiveDSEntity. state conn eid meta validator watches)) | |
IAtom | |
IReset | |
(-reset! [this new-val] | |
(let [old-value (.-state this)] | |
(when-not (identical? old-value new-val) | |
(let [validate (.-validator this)] | |
(when-not (nil? validate) | |
(assert (validate new-val) "Validator rejected reference state."))) | |
(entity-reset! conn eid new-val) | |
(set! (.-state this) new-val) | |
(when-not (empty? (.-watches this)) | |
(-notify-watches this old-value new-val))) | |
new-val)) | |
ISwap | |
(-swap! [this f] (-reset! this (f (.-state this)))) | |
(-swap! [this f x] (-reset! this (f (.-state this) x))) | |
(-swap! [this f x y] (-reset! this (f (.-state this) x y))) | |
(-swap! [this f x y more] (-reset! this (apply f (.-state this) x y more))) | |
IDeref | |
(-deref [this] | |
(when-let [invalidate *invalidate-rx*] | |
(-add-watch this invalidate invalidate) | |
(when *trace-capture* (*trace-capture* this))) | |
state) | |
IReactive | |
(-raw-deref [this] state) | |
IWatchable | |
(-notify-watches [this oldval newval] | |
(doseq [[key f] watches] | |
(f key this oldval newval))) | |
(-add-watch [this key f] | |
(set! (.-watches this) (assoc watches key f)) | |
this) | |
(-remove-watch [this key] | |
(set! (.-watches this) (dissoc watches key))) | |
IPrintWithWriter | |
(-pr-writer [a writer opts] | |
(-write writer "#<ReactiveDSEntity: ") | |
(pr-writer state writer opts) | |
(-write writer ">"))) | |
(defn eid [ent] (.-eid ent)) | |
(defn entity [conn eid] | |
(let [ent-map (ent->map (d/entity @conn eid)) | |
r-ent (->ReactiveDSEntity ent-map conn eid nil nil nil)] | |
(d/listen! conn (hash r-ent) | |
(fn [{:keys [tx-data]}] | |
(when (some #{eid} (map first tx-data)) | |
(reset! r-ent (ent->map (d/entity @conn eid)))))) | |
r-ent)) | |
(comment | |
(def mike (entity conn 1)) | |
(def mikes-job (f/cursor mike :job)) | |
@mike ;; => {:job "Astronaut", :name "Mike"} | |
@mikes-job ;; => "Astronaut" | |
(swap! mike assoc :hobby "Jogging") | |
(reset! mikes-job "Postman") | |
@mike ;; => {:job "Postman", :name "Mike", :hobby "Jogging"} | |
) | |
;;; Using cursors with the connection ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(defn db-getter | |
"Returns a getter function for the DataScript conn to be used with `cursor`." | |
[conn [eid & attrs]] | |
{:pre [(number? eid)]} | |
(fn [db] | |
(let [ent-map (ent->map (d/entity db eid))] | |
(not-empty | |
(if attrs | |
(get-in ent-map attrs) | |
ent-map))))) | |
(defn db-setter | |
"Returns a setter function for the DataScript conn to be used with `cursor`." | |
[conn [eid & attrs]] | |
{:pre [(number? eid)]} | |
(fn [db new-val] | |
(:db-after | |
(if-let [[f & r] attrs] | |
(d/transact! conn | |
(if-not r | |
[[:db/add eid f new-val]] | |
(let [old (get (ent->map (d/entity @conn eid)) f)] | |
[[:db/add eid f (assoc-in old r new-val)]]))) | |
(entity-reset! conn eid new-val))))) | |
(defn db-cursor | |
"Creates a cursor into a DataScript connection. Allows you to treat entities | |
and their attributes as though they are cursors to a map. Path can be either a | |
number (entity-id) or a sequential beginning with a number." | |
[conn path] | |
(cond | |
(number? path) | |
(f/cursor conn (db-getter conn [path]) (db-setter conn [path])) | |
(sequential? path) | |
(do (assert (number? (first path)) "Path to DB must begin with the entity-id.") | |
(f/cursor conn (db-getter conn path) (db-setter conn path))) | |
:else (throw js/Error "Path must be a number (entity-id) or a collection."))) | |
(comment | |
(def joe (db-cursor conn 2)) | |
(def joes-job (db-cursor conn [2 :job])) | |
@joe ;; => {:job "Janitor", :name "Joel"} | |
@joes-job ;; => "Janitor" | |
(swap! joe assoc :hobby "Biking") | |
(reset! joes-job "CEO") | |
@joe ;; => {:hobby "Biking", :job "CEO", :name "Joel"} | |
) | |
;;; Reactive Connection ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(defn create-conn | |
"Like `datascript/create-conn` but uses a reactive atom to store the db." | |
[& [schema]] | |
(f/atom (d/empty-db schema) | |
:meta {:listeners (atom {})})) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment