Skip to content

Instantly share code, notes, and snippets.

@luxbock
Created December 10, 2014 16:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save luxbock/d2693807e8622c11684c to your computer and use it in GitHub Desktop.
Save luxbock/d2693807e8622c11684c to your computer and use it in GitHub Desktop.
freactive-datascript
(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