diff --git a/deps.edn b/deps.edn
index 7fcb3c8b..d0693a7b 100644
--- a/deps.edn
+++ b/deps.edn
@@ -2,9 +2,10 @@
:deps {thheller/shadow-cljs {:mvn/version "2.11.25"}
devcards/devcards {:mvn/version "0.2.7"}
datascript/datascript {:mvn/version "1.0.7"}
- reagent/reagent {:mvn/version "1.0.0-alpha2"}
+ reagent/reagent {:mvn/version "1.0.0"}
inflections/inflections {:mvn/version "0.13.2"}
binaryage/devtools {:mvn/version "1.0.2"}
- homebaseio/datalog-console {:git/url "https://github.com/homebaseio/datalog-console" :sha "97d5e5eb8994124ec8dc0029b33f2e88257b39b2"}
+ homebaseio/datalog-console {:git/url "https://github.com/homebaseio/datalog-console" :sha "fc4cf9ba968e995a67aae1816670adb78a81451d"}
;; homebaseio/datalog-console {:local/root "../datalog-console"}
+ nano-id {:mvn/version "1.0.0"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}}}
diff --git a/src/dev/example/core.cljs b/src/dev/example/core.cljs
index 11e834d9..5ea2e656 100644
--- a/src/dev/example/core.cljs
+++ b/src/dev/example/core.cljs
@@ -6,10 +6,12 @@
[cljsjs.react.dom]
[reagent.core]
[devcards.core :as dc]
- [dev.example.array]
- [dev.example.counter]
- [dev.example.todo]
- [dev.example.todo-firebase]))
+ [dev.example.react.array]
+ [dev.example.react.counter]
+ [dev.example.react.todo]
+ [dev.example.react.todo-firebase]
+ [dev.example.reagent.counter]
+ [dev.example.reagent.todo]))
(js/goog.exportSymbol "marked" marked)
(js/goog.exportSymbol "DevcardsMarked" marked)
diff --git a/src/dev/example/js/todo.jsx b/src/dev/example/js/todo.jsx
index 6c89c9eb..09585d6e 100644
--- a/src/dev/example/js/todo.jsx
+++ b/src/dev/example/js/todo.jsx
@@ -163,7 +163,7 @@ const Todo = React.memo(({ id }) => {
·
- {todo.get('createdAt').toLocaleString()}
+ {todo.get('createdAt')?.toLocaleString()}
)
})
@@ -237,7 +237,8 @@ const TodoFilters = () => {
type="checkbox"
checked={filters.get('showCompleted')}
onChange={(e) =>
- transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked } }])}
+ transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked } }])
+ }
/>
·
diff --git a/src/dev/example/js_compiled/todo.js b/src/dev/example/js_compiled/todo.js
index 9f615b82..b68adaf3 100644
--- a/src/dev/example/js_compiled/todo.js
+++ b/src/dev/example/js_compiled/todo.js
@@ -171,7 +171,7 @@ const Todo = /*#__PURE__*/_react.default.memo(({
style: {
color: 'grey'
}
- }, todo.get('createdAt').toLocaleString()));
+ }, todo.get('createdAt')?.toLocaleString()));
});
const TodoCheck = ({
diff --git a/src/dev/example/array.cljs b/src/dev/example/react/array.cljs
similarity index 89%
rename from src/dev/example/array.cljs
rename to src/dev/example/react/array.cljs
index 13de02e3..32ec1947 100644
--- a/src/dev/example/array.cljs
+++ b/src/dev/example/react/array.cljs
@@ -1,8 +1,8 @@
-(ns dev.example.array
+(ns dev.example.react.array
(:require
[devcards.core :as dc]
[homebase.react]
- ["./js_compiled/array" :as react-example])
+ ["../js_compiled/array" :as react-example])
(:require-macros
[devcards.core :refer [defcard-rg defcard-doc]]
[dev.macros :refer [inline-resource]]))
diff --git a/src/dev/example/counter.cljs b/src/dev/example/react/counter.cljs
similarity index 89%
rename from src/dev/example/counter.cljs
rename to src/dev/example/react/counter.cljs
index c4617d9c..e9c9a610 100644
--- a/src/dev/example/counter.cljs
+++ b/src/dev/example/react/counter.cljs
@@ -1,8 +1,8 @@
-(ns dev.example.counter
+(ns dev.example.react.counter
(:require
[devcards.core :as dc]
[homebase.react]
- ["./js_compiled/counter" :as react-example])
+ ["../js_compiled/counter" :as react-example])
(:require-macros
[devcards.core :refer [defcard-rg defcard-doc]]
[dev.macros :refer [inline-resource]]))
diff --git a/src/dev/example/todo.cljs b/src/dev/example/react/todo.cljs
similarity index 91%
rename from src/dev/example/todo.cljs
rename to src/dev/example/react/todo.cljs
index 81f6ace6..93c8ffa4 100644
--- a/src/dev/example/todo.cljs
+++ b/src/dev/example/react/todo.cljs
@@ -1,8 +1,8 @@
-(ns dev.example.todo
+(ns dev.example.react.todo
(:require
[devcards.core :as dc]
[homebase.react]
- ["./js_compiled/todo" :as react-example])
+ ["../js_compiled/todo" :as react-example])
(:require-macros
[devcards.core :refer [defcard-rg defcard-doc]]
[dev.macros :refer [inline-resource]]))
diff --git a/src/dev/example/todo_firebase.cljs b/src/dev/example/react/todo_firebase.cljs
similarity index 88%
rename from src/dev/example/todo_firebase.cljs
rename to src/dev/example/react/todo_firebase.cljs
index 6ad72fac..e4ce5723 100644
--- a/src/dev/example/todo_firebase.cljs
+++ b/src/dev/example/react/todo_firebase.cljs
@@ -1,8 +1,8 @@
-(ns dev.example.todo-firebase
+(ns dev.example.react.todo-firebase
(:require
[devcards.core :as dc]
[homebase.react]
- ["./js_compiled/todo-firebase" :as react-example])
+ ["../js_compiled/todo-firebase" :as react-example])
(:require-macros
[devcards.core :refer [defcard-rg defcard-doc]]
[dev.macros :refer [inline-resource]]))
diff --git a/src/dev/example/reagent/counter.cljs b/src/dev/example/reagent/counter.cljs
new file mode 100644
index 00000000..216925de
--- /dev/null
+++ b/src/dev/example/reagent/counter.cljs
@@ -0,0 +1,27 @@
+(ns dev.example.reagent.counter
+ (:require
+ [devcards.core :as dc]
+ [datascript.core :as d]
+ [homebase.reagent :as hbr])
+ (:require-macros
+ [devcards.core :refer [defcard-rg defcard-doc]]
+ [dev.macros :refer [inline-resource]]))
+
+(def db-conn (d/create-conn {}))
+(hbr/connect! db-conn)
+(d/transact! db-conn [[:db/add 1 :count 0]])
+
+(defn counter []
+ (let [[e] (hbr/entity db-conn 1)]
+ (fn []
+ [:div
+ "Count: " (:count @e)
+ [:div
+ [:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @e))]])}
+ "Increment"]]])))
+
+(defcard-rg counter-example
+ counter)
+
+(defcard-doc
+ (str "```clojure\n" (inline-resource "src/dev/example/reagent/counter.cljs") "\n```"))
\ No newline at end of file
diff --git a/src/dev/example/reagent/todo.cljs b/src/dev/example/reagent/todo.cljs
new file mode 100644
index 00000000..4cffe860
--- /dev/null
+++ b/src/dev/example/reagent/todo.cljs
@@ -0,0 +1,186 @@
+(ns dev.example.reagent.todo
+ (:require
+ [devcards.core :as dc]
+ [datascript.core :as d]
+ [reagent.core :as r]
+ [homebase.reagent :as hbr])
+ (:require-macros
+ [devcards.core :refer [defcard-rg defcard-doc]]
+ [dev.macros :refer [inline-resource]]))
+
+(def db-conn (d/create-conn {:todo/project {:db/type :db.type/ref
+ :db/cardinality :db.cardinality/one}
+ :todo/owner {:db/type :db.type/ref
+ :db/cardinality :db.cardinality/one}}))
+(hbr/connect! db-conn)
+(d/transact! db-conn [{:todo/name "Go home"
+ :todo/created-at (js/Date.now)
+ :todo/owner -2
+ :todo/project -3}
+ {:todo/name "Fix ship"
+ :todo/completed? true
+ :todo/created-at (js/Date.now)
+ :todo/owner -1
+ :todo/project -4}
+ {:db/id -1
+ :user/name "Stella"}
+ {:db/id -2
+ :user/name "Arpegius"}
+ {:db/id -3
+ :project/name "Do it"}
+ {:db/id -4
+ :project/name "Make it"}])
+
+#_(dotimes [n 1000]
+ (d/transact! db-conn [{:todo/name (str n)
+ :todo/created-at (js/Date.now)}]))
+
+(defn select [{:keys [label attr value on-change]}]
+ (let [[options] (hbr/q '[:find ?e ?v
+ :in $ ?attr
+ :where [?e ?attr ?v]]
+ db-conn attr)]
+ (fn [{:keys [label attr value on-change]}]
+ [:label label " "
+ [:select
+ {:name (str attr)
+ :value (or value "")
+ :on-change (fn [e] (when on-change (on-change (js/Number (goog.object/getValueByKeys e #js ["target" "value"])))))}
+ [:option {:value ""} ""]
+ (for [[id value] @options]
+ ^{:key id} [:option
+ {:value id}
+ value])]])))
+
+(defn test-rev-ref [id]
+ (let [[todo] (hbr/entity db-conn id)]
+ (fn []
+ [:div
+ [:button
+ {:on-click #(d/transact! db-conn [[:db/add (:db/id (:todo/owner @todo)) :user/name (str (rand-int 99))]])}
+ "change rev ref name"]
+ (:user/name (:todo/owner (first (:todo/_owner (:todo/owner @todo)))))])))
+
+(defn test-ref [id]
+ (let [[todo] (hbr/entity db-conn id)]
+ (fn []
+ [:div
+ [:button
+ {:on-click #(d/transact! db-conn [[:db/add (:db/id (:todo/owner @todo)) :user/name (str (rand-int 99))]])}
+ "change ref name"]
+ (:user/name (:todo/owner @todo))])))
+
+(defn todo [id]
+ (let [[todo] (hbr/entity db-conn id)]
+ (fn []
+ [:div {:style {:padding-bottom 20}}
+ [test-ref id]
+ [test-rev-ref id]
+ [:div
+ [:input
+ {:type "checkbox"
+ :style {:width "18px" :height "18px" :margin-left "0"}
+ :checked (true? (:todo/completed? @todo))
+ :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}]
+ [:input
+ {:type "text"
+ :style {:text-decoration (when (:todo/completed? @todo) "line-through") :border "none" :width "auto" :font-weight "bold" :font-size "20px"}
+ :value (:todo/name @todo)
+ :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/name (goog.object/getValueByKeys % #js ["target" "value"])]])}]]
+ [:div
+ [select
+ {:label "Owner:"
+ :attr :user/name
+ :value (get-in @todo [:todo/owner :db/id])
+ :on-change (fn [owner-id] (d/transact! db-conn [[(if (= 0 owner-id) :db/retract :db/add) (:db/id @todo) :todo/owner (when (not= 0 owner-id) owner-id)]]))}]
+ " · "
+ [select
+ {:label "Project:"
+ :attr :project/name
+ :value (get-in @todo [:todo/project :db/id])
+ :on-change (fn [project-id] (d/transact! db-conn [[(if (= 0 project-id) :db/retract :db/add) (:db/id @todo) :todo/project (when (not= 0 project-id) project-id)]]))}]
+ " · "
+ [:button
+ {:on-click #(d/transact! db-conn [[:db/retractEntity (:db/id @todo)]])}
+ "Delete"]]
+ [:div
+ [:small {:style {:color "grey"}}
+ (.toLocaleString (js/Date. (:todo/created-at @todo)))]]])))
+
+(defn todo-filters [filters]
+ [:div {:style {:padding "20px 0"}}
+ [:strong "Filters · "]
+ [:label
+ "Show completed "
+ [:input
+ {:type "checkbox"
+ :checked (:f-all? @filters)
+ :on-change #(swap! filters assoc :f-all? (goog.object/getValueByKeys % #js ["target" "checked"]))}]]
+ " · "
+ [select
+ {:label "Owner"
+ :attr :user/name
+ :value (:f-owner @filters)
+ :on-change (fn [owner-id]
+ (swap! filters assoc :f-owner
+ (when (not= 0 owner-id) owner-id)))}]
+ " · "
+ [select
+ {:label "Project"
+ :attr :project/name
+ :value (:f-project @filters)
+ :on-change (fn [project-id]
+ (swap! filters assoc :f-project
+ (when (not= 0 project-id) project-id)))}]
+ ])
+
+(defn todos [filters]
+ (let [[todos] (hbr/q '[:find [(pull ?todo [:db/id :todo/created-at :todo/project :todo/owner :todo/completed?]) ...]
+ :where
+ [?todo :todo/name]]
+ db-conn)]
+ (fn [filters]
+ (let [{:keys [f-project f-owner f-all?]} @filters]
+ [:div
+ [todo-filters filters]
+ [:div
+ (doall
+ (for [{:keys [db/id]}
+ (->> @todos
+ (remove (fn [{:keys [todo/project todo/owner todo/completed?]}]
+ (or (and f-project (not= (:db/id project) f-project))
+ (and f-owner (not= (:db/id owner) f-owner))
+ (and (not f-all?) completed?))))
+ (sort-by :todo/created-at)
+ (reverse))]
+ ^{:key id} [todo id]))]]))))
+
+(def default-filters {:f-project nil :f-owner nil :f-all? true})
+
+(defn new-todo [filters]
+ (let [name (r/atom "")]
+ (fn [filters]
+ [:form {:on-submit (fn [e]
+ (.preventDefault e)
+ (d/transact! db-conn [{:todo/name @name
+ :todo/created-at (js/Date.now)}])
+ (reset! name "")
+ (reset! filters default-filters))}
+ [:input {:type "text"
+ :on-change #(reset! name (goog.object/getValueByKeys % #js ["target" "value"]))
+ :value @name
+ :placeholder "Write a todo..."}]
+ [:button {:type "submit"} "Create todo"]])))
+
+(defn todo-app []
+ (let [filters (r/atom default-filters)]
+ (fn []
+ [:div
+ [new-todo filters]
+ [todos filters]])))
+
+(defcard-rg todo-example
+ todo-app)
+
+(defcard-doc
+ (str "```clojure\n" (inline-resource "src/dev/example/reagent/todo.cljs") "\n```"))
\ No newline at end of file
diff --git a/src/homebase/cache.cljs b/src/homebase/cache.cljs
new file mode 100644
index 00000000..560224e8
--- /dev/null
+++ b/src/homebase/cache.cljs
@@ -0,0 +1,84 @@
+(ns homebase.cache
+ (:require
+ [datascript.core :as datascript]
+ [datascript.db]))
+
+(defn create-conn []
+ (atom
+ {:ea {}
+ :q {}}))
+
+(defn assoc-ea
+ [cache eid-attr-tuple reactive-lookup-uid change-handler]
+ (assoc-in cache [:ea eid-attr-tuple reactive-lookup-uid] change-handler))
+
+(defn dissoc-ea
+ [cache eid-attr-tuple reactive-lookup-uid]
+ (let [cache (update-in cache [:ea eid-attr-tuple] dissoc reactive-lookup-uid)]
+ (if (empty? (get-in cache [:ea eid-attr-tuple]))
+ (update cache :ea dissoc eid-attr-tuple)
+ cache)))
+
+(defn assoc-q
+ [cache query reactive-lookup-uid change-handler]
+ (assoc-in cache [:q query reactive-lookup-uid] change-handler))
+
+(defn dissoc-q
+ [cache query reactive-lookup-uid]
+ (let [cache (update-in cache [:q query] dissoc reactive-lookup-uid)]
+ (if (empty? (get-in cache [:q query]))
+ (update cache :q dissoc query)
+ cache)))
+
+(defn create-listener
+ "Returns a listener function that invokes all subscribed change-handlers in the cache when a datom is transacted."
+ [cache-conn]
+ (fn [{:keys [tx-data]}]
+ (let [cache @cache-conn
+ ;; The EA change-handler only needs to be triggered once for each reactive-lookup-uid.
+ triggered-ea-handlers (atom #{})]
+ ;; EA handlers
+ (doseq [[e a :as datom] tx-data]
+ (let [subscriptions (get-in cache [:ea [e a]])]
+ (doseq [[reactive-lookup-uid change-handler] subscriptions]
+ (when (not (get @triggered-ea-handlers reactive-lookup-uid))
+ (swap! triggered-ea-handlers conj reactive-lookup-uid)
+ (change-handler {:datom datom
+ :reactive-lookup-uid reactive-lookup-uid})))))
+ ;; Query handlers
+ ;; TODO: dispatch on change-handlers more judiciously instead of on every transaction.
+ ;; See work on incremental view manintinence e.g. https://github.com/sixthnormal/clj-3df
+ (let [subscriptions (mapcat seq (vals (:q cache)))]
+ (doseq [[reactive-lookup-uid change-handler] subscriptions]
+ (change-handler {:reactive-lookup-uid reactive-lookup-uid}))))))
+
+(defn db-conn-type [db-conn]
+ (if (instance? cljs.core/Atom db-conn)
+ (type @db-conn)
+ (type db-conn)))
+
+(defmulti connect!
+ "Connect the cache to a database connection and listen to changes in the transaction log."
+ (fn [cache-conn db-conn] (db-conn-type db-conn)))
+(defmethod connect! datascript.db/DB [cache-conn db-conn]
+ (swap! db-conn with-meta (merge (meta @db-conn) {::conn cache-conn}))
+ (datascript/listen! db-conn ::connection (create-listener cache-conn)))
+
+(defmulti disconnect!
+ "Disconnect the transaction log listener."
+ (fn [db-conn] (db-conn-type db-conn)))
+(defmethod disconnect! datascript.db/DB [db-conn]
+ (swap! db-conn with-meta (dissoc (meta @db-conn) ::conn))
+ (datascript/unlisten! db-conn ::connection))
+
+(comment
+ (do
+ (def cache-conn (create-conn))
+ (def db-conn (datascript/create-conn {}))
+ (connect! cache-conn db-conn)
+ (swap! cache-conn assoc-ea [1 :a] "abc123" #(print "yolo" %)))
+ (datascript/transact! db-conn [{:a "a" :b "b" :c "c"}])
+ (datascript/transact! db-conn [[:db/retract 1 :a]])
+ (datascript/transact! db-conn [[:db/retractEntity 1]])
+ (swap! cache-conn dissoc-ea [1 :a] "abc123")
+ (disconnect! db-conn))
\ No newline at end of file
diff --git a/src/homebase/js.cljs b/src/homebase/js.cljs
index e83f6fb8..5a193d7c 100644
--- a/src/homebase/js.cljs
+++ b/src/homebase/js.cljs
@@ -136,8 +136,9 @@
{} (js->clj lookup))))
(defmulti js->entity-lookup type)
-(defmethod js->entity-lookup js/Number [lookup] lookup)
(defmethod js->entity-lookup js/Object [lookup] (first (js->object-lookup lookup)))
+(defmethod js->entity-lookup js/Number [lookup] lookup)
+(defmethod js->entity-lookup :default [lookup] lookup)
(comment
(js->tx nil (clj->js [{:project {:array [[1] [2 {:k "v"}]]}}]))
@@ -228,14 +229,21 @@
(reduced (namespace k))))
nil (keys entity)))
-(defn js-get [^de/Entity entity name]
+(defn js-guess-attr
+ "Takes an entity and a js name string and trys to guess the
+ ns and cljs name of the corresponding attribute in the given entity.
+
+ Assumes that the entity was created by homebase.js and conforms to its conventions.
+
+ Returns a keyword."
+ [^de/Entity entity name]
(case name
- "id" (:db/id entity)
- "ident" (:db/ident entity)
- "identity" (:db/ident entity)
+ "id" :db/id
+ "ident" :db/ident
+ "identity" :db/ident
(let [maybe-ns (guess-entity-ns entity)
k (when maybe-ns (js->key maybe-ns name))]
- (when k (get entity k)))))
+ k)))
(declare
Entity
@@ -284,9 +292,10 @@
(defn lookup-entity
"Takes a homebase.js/Entity and a seq of attributes. Looks up the attribute path on the entity. Returns a scalar or homebase.js/Entity or js/Array of scalars or Entities."
- ([entity attrs] (lookup-entity entity attrs false))
- ([entity attrs nil-attrs-if-not-in-db?] (lookup-entity entity attrs nil-attrs-if-not-in-db? nil))
- ([entity attrs nil-attrs-if-not-in-db? get-cb]
+ ([entity attrs] (lookup-entity entity attrs false nil nil))
+ ([entity attrs nil-attrs-if-not-in-db?] (lookup-entity entity attrs nil-attrs-if-not-in-db? nil nil))
+ ([entity attrs nil-attrs-if-not-in-db? get-cb] (lookup-entity entity attrs nil-attrs-if-not-in-db? get-cb nil))
+ ([entity attrs nil-attrs-if-not-in-db? get-cb after-lookup]
(humanize-error
#(humanize-get-error % entity)
(fn []
@@ -295,8 +304,13 @@
(if-not acc
nil
(let [attr (keywordize attr)
- getter-fn (if (keyword? attr) get js-get)
- getter-fn (comp (partial entity->js {:Entity/get-cb get-cb})
+ getter-fn (fn [entity attr]
+ (let [attr (if (keyword? attr) attr (js-guess-attr entity attr))
+ result (when attr (get entity attr))]
+ (when (and after-lookup attr) (after-lookup {:entity entity :attr attr :result result }))
+ result))
+ getter-fn (comp (partial entity->js {:Entity/get-cb get-cb
+ ::after-lookup after-lookup})
getter-fn)
result (cond
(array? acc) (if (number? attr)
@@ -333,17 +347,11 @@
(-contains-key? [this k] (not (nil? (lookup-entity this [k] true))))
Object
(get [this & attrs]
- (let [get-cb (:Entity/get-cb (meta this))
- v (lookup-entity this attrs true get-cb)]
+ (let [{:keys [:Entity/get-cb ::after-lookup]} (meta this)
+ v (lookup-entity this attrs true get-cb after-lookup)]
(when get-cb (get-cb [this attrs v]))
v)))
-(defn q-entity-array [query conn & args]
- (->> (apply d/q query conn args)
- (map (fn id->entity [[id]]
- (new-entity (d/entity conn id) nil)))
- to-array))
-
(defn transact!
([conn tx] (transact! conn tx nil))
([conn tx tx-meta]
@@ -359,7 +367,13 @@
(defn q [query conn & args]
(humanize-error
humanize-q-error
- #(apply q-entity-array (js->query query) @conn (keywordize args))))
+ #(apply d/q (js->query query) @conn (keywordize args))))
+
+(defn ids->entities [db ids]
+ (->> ids
+ (map (fn id->entity [[id]]
+ (new-entity (d/entity db id) nil)))
+ to-array))
(defn humanize-get-error [error entity]
(condp re-find (goog.object/get error "message")
diff --git a/src/homebase/react.cljs b/src/homebase/react.cljs
index 70dd95b7..67ea0e7e 100644
--- a/src/homebase/react.cljs
+++ b/src/homebase/react.cljs
@@ -6,12 +6,12 @@
[goog.object]
[clojure.set]
[homebase.js :as hbjs]
+ [homebase.cache :as hbc]
+ [nano-id.core :refer [nano-id]]
[datascript.core :as d]
[datascript.impl.entity :as de]
[homebase.datalog-console :as datalog-console]))
-
-
(defn try-hook [hook-name f]
(if hbjs/*debug*
(f)
@@ -28,85 +28,6 @@
(second)
(clojure.string/trim))))))))))
-(defn debug-msg [return-value & msgs]
- (when (and (number? hbjs/*debug*) (>= hbjs/*debug* 2))
- (apply js/console.log "%c homebase-react " "background: yellow" msgs))
- return-value)
-
-(defn changed? [entities cached-entities track-count?]
- (cond
- (and track-count?
- (not= (count entities) (count cached-entities)))
- (debug-msg true "cache:miss" "count of entities != cache"
- #js {:entities (clj->js entities)
- :cache (clj->js cached-entities)})
-
- (and track-count?
- (not (clojure.set/superset?
- (set (keys cached-entities))
- (set (map #(get % "id") entities)))))
- (debug-msg true "cache:miss" "cache not superset of entities"
- #js {:entities (clj->js entities)
- :cache (clj->js cached-entities)})
-
- :else
- (reduce
- (fn [_ e]
- (when (let [id (get e "id")
- cached-e (get cached-entities id)]
- (if (nil? cached-e)
- (if-not id
- (reduced false) ; This entity has probably been removed, do not force a rerender
- (reduced (debug-msg true "cache:miss" "not in cache"
- #js {:entity-id id
- :entities (clj->js entities)
- :cache (clj->js cached-entities)})))
- (reduce (fn [_ [ks old-v]]
- (let [e-without-cache (hbjs/Entity. ^de/Entity (.-_entity e) nil nil nil nil)
- new-v (.apply (.-get e-without-cache) e-without-cache (into-array ks))]
- (when (and (not= 0 (compare old-v new-v))
- ;; Ignore Entities and arrays of Entities
- (not (or (instance? hbjs/Entity new-v)
- (and (array? new-v)
- (= (count new-v) (count old-v))
- (instance? hbjs/Entity (nth new-v 0))))))
- (reduced (debug-msg true "cache:miss" "value changed"
- #js {:entity-id id
- :attr-path (clj->js ks)
- :e e
- :old-v old-v
- :new-v new-v
- :entities (clj->js entities)
- :cache (clj->js cached-entities)})))))
- nil cached-e)))
- (reduced true)))
- nil entities)))
-
-(defn cache->js [entity cached-entities]
- (reduce
- (fn [acc [ks v]]
- (goog.object/set acc (str (to-array ks)) v)
- acc)
- #js {} (get @cached-entities (get entity "id"))))
-
-(defn touch-entity-cache [entity cached-entities]
- (let [get-cb (fn [[e ks v]]
- (if (get e "id")
- (do
- (swap! cached-entities assoc-in [(get e "id") ks] v)
- (when hbjs/*debug*
- (set! ^js/Object (.-_recentlyTouchedAttributes entity)
- (cache->js e cached-entities))))
- (do
- (reset! cached-entities {})
- (when hbjs/*debug*
- (set! ^js/Object (.-_recentlyTouchedAttributes entity) #js {})))))
- _ (when hbjs/*debug* (set! ^js/Object (.-_recentlyTouchedAttributes entity) #js {}))
- ; Use (set! ...) instead of (vary-meta) to preserve the reference to the original entity
- ;; entity (vary-meta entity merge {:Entity/get-cb get-cb})
- _ (set! ^hbjs/Entity (.-_meta entity) {:Entity/get-cb get-cb})]
- entity))
-
(defn datom-select-keys [d]
#js [(:e d) (str (:a d)) (:v d) (:tx d) (:added d)])
@@ -117,7 +38,7 @@
;; (defn datoms->json [datoms]
;; (reduce
-;; (fn [acc {:keys [e a v]}]
+;; (fn [acc {:keys [e a v]}]
;; (assoc-in acc [e (namespace a) (name a)] v))
;; {} datoms))
@@ -134,88 +55,158 @@
initial-tx (goog.object/getValueByKeys props #js ["config" "initialData"])
debug (goog.object/getValueByKeys props #js ["config" "debug"])
_ (when debug (set! hbjs/*debug* debug))
- conn (d/create-conn (if schema
- (merge (hbjs/js->schema schema) base-schema)
- base-schema))]
- (datalog-console/init! {:conn conn})
- (when initial-tx (hbjs/transact! conn initial-tx))
- (react/createElement
- (goog.object/get homebase-context "Provider")
- #js {:value conn}
- (goog.object/get props "children"))))
+ [initializing? setInitializing?] (react/useState true)
+ db-conn (react/useMemo
+ #(d/create-conn (if schema
+ (merge (hbjs/js->schema schema) base-schema)
+ base-schema))
+ #js [])
+ cache-conn (react/useMemo
+ #(hbc/create-conn)
+ #js [])]
+ (react/useEffect
+ (fn []
+ (hbc/connect! cache-conn db-conn)
+ (datalog-console/init! {:conn db-conn})
+ (when initial-tx (hbjs/transact! db-conn initial-tx))
+ (setInitializing? false)
+ #(hbc/disconnect! db-conn))
+ #js [])
+ (if initializing?
+ ""
+ (react/createElement
+ (goog.object/get homebase-context "Provider")
+ #js {:value #js {:db-conn db-conn :cache-conn cache-conn}}
+ (goog.object/get props "children")))))
(defn ^:export useClient []
- (let [conn (react/useContext homebase-context)
+ (let [{:strs [db-conn]} (js->clj (react/useContext homebase-context))
key (react/useMemo rand #js [])
client (react/useMemo
(fn []
- #js {"dbToString" #(pr-str @conn)
- "dbFromString" #(do (reset! conn (cljs.reader/read-string %))
- (d/transact! conn [] ::silent))
- "dbToDatoms" #(datoms->js (d/datoms @conn :eavt))
- ;; "dbToJSON" #(clj->js (datoms->json (d/datoms @conn :eavt)))
- "entity" (fn [lookup] (js/Promise.resolve (hbjs/entity conn lookup)))
- "query" (fn [query & args] (js/Promise.resolve (apply hbjs/q query conn args)))
- "transactSilently" (fn [tx] (try-hook "useClient" #(hbjs/transact! conn tx ::silent)))
- "addTransactListener" (fn [listener-fn] (d/listen! conn key #(when (not= ::silent (:tx-meta %))
+ #js {"dbToString" #(pr-str @db-conn)
+ "dbFromString" #(do (reset! db-conn (cljs.reader/read-string %))
+ (d/transact! db-conn [] ::silent))
+ "dbToDatoms" #(datoms->js (d/datoms @db-conn :eavt))
+ ;; "dbToJSON" #(clj->js (datoms->json (d/datoms @db-conn :eavt)))
+ "entity" (fn [lookup] (js/Promise.resolve (hbjs/entity db-conn lookup)))
+ "query" (fn [query & args] (js/Promise.resolve (apply hbjs/q query db-conn args)))
+ "transactSilently" (fn [tx] (try-hook "useClient" #(hbjs/transact! db-conn tx ::silent)))
+ "addTransactListener" (fn [listener-fn] (d/listen! db-conn key #(when (not= ::silent (:tx-meta %))
(listener-fn (datoms->js (:tx-data %))))))
- "removeTransactListener" #(d/unlisten! conn key)})
+ "removeTransactListener" #(d/unlisten! db-conn key)})
#js [])]
- [client]))
-
+ #js [client]))
+
(defn ^:export useEntity [lookup]
- (let [conn (react/useContext homebase-context)
- cached-entities (react/useMemo #(atom {}) #js [])
- run-lookup (react/useCallback
- (fn run-lookup []
- (touch-entity-cache
- (try-hook "useEntity" #(hbjs/entity conn lookup))
- cached-entities))
- #js [lookup])
- [result setResult] (react/useState (run-lookup))
- listener (react/useCallback
- (fn entity-listener []
- (let [result (run-lookup)]
- (when (changed? #js [result] @cached-entities false)
- (setResult result))))
- #js [run-lookup])]
+ (let [{:strs [db-conn cache-conn]} (js->clj (react/useContext homebase-context))
+ hbjs-entity (try-hook "useEntity" #(hbjs/entity db-conn lookup))
+ reactive-lookup-uid (react/useMemo #(nano-id) #js [])
+ tracked-ea-pairs (react/useMemo #(atom #{}) #js [])
+ [result setResult] (react/useState hbjs-entity)
+ after-lookup (react/useCallback
+ (fn after-lookup [{:keys [^de/Entity entity attr]}]
+ (swap! tracked-ea-pairs conj [(:db/id entity) attr])
+ (swap! cache-conn hbc/assoc-ea [(:db/id entity) attr] reactive-lookup-uid
+ (fn change-handler []
+ (let [entity (try-hook "useEntity" #(hbjs/entity db-conn lookup))
+ _ (set! ^hbjs/Entity (.-_meta entity)
+ {:homebase.js/after-lookup (:homebase.js/after-lookup (meta result))})]
+ (setResult entity))))
+ #_(js/console.log "after lookup" lookup (:db/id entity) attr @cache-conn))
+ #js [result setResult lookup])
+ _ (set! ^hbjs/Entity (.-_meta hbjs-entity)
+ {:homebase.js/after-lookup after-lookup})]
(react/useEffect
- (fn use-entity-effect []
- (let [key (rand)]
- (d/listen! conn key listener)
- #(d/unlisten! conn key)))
- #js [lookup])
- [result]))
+ (fn []
+ #(doseq [ea @tracked-ea-pairs]
+ (swap! cache-conn hbc/dissoc-ea ea reactive-lookup-uid)
+ #_(js/console.log "dissoc-ea" ea @cache-conn)))
+ #js [])
+ #js [result]))
(defn ^:export useQuery [query & args]
- (let [conn (react/useContext homebase-context)
- cached-entities (react/useMemo #(atom {}) #js [])
- run-query (react/useCallback
- (fn run-query []
- (let [result (try-hook "useQuery" #(apply hbjs/q query conn args))]
- (when (and (not= (count result) (count @cached-entities))
- (not= 0 (count result)))
- (reset! cached-entities {}))
- (.map result (fn [e] (touch-entity-cache e cached-entities)))))
- #js [query args])
- [result setResult] (react/useState (run-query))
- listener (react/useCallback
- (fn query-listener []
- (let [result (run-query)]
- (when (changed? result @cached-entities true)
- (setResult result))))
- #js [run-query])]
+ (let [{:strs [db-conn cache-conn]} (js->clj (react/useContext homebase-context))
+ reactive-lookup-uid (react/useMemo #(nano-id) #js [query args])
+ q-result (atom (try-hook "useQuery" #(apply hbjs/q query db-conn args)))
+ tracked-ea-pairs (react/useMemo #(atom #{}) #js [])
+ [result setResult] (react/useState (hbjs/ids->entities @db-conn @q-result))
+ after-lookup (react/useCallback
+ (fn after-lookup [{:keys [^de/Entity entity attr]}]
+ (swap! tracked-ea-pairs conj [(:db/id entity) attr])
+ (swap! cache-conn hbc/assoc-ea [(:db/id entity) attr] reactive-lookup-uid
+ (fn change-handler []
+ (let [entity (try-hook "useEntity" #(hbjs/entity db-conn lookup))
+ _ (set! ^hbjs/Entity (.-_meta entity)
+ {:homebase.js/after-lookup (:homebase.js/after-lookup (meta result))})]
+ (setResult entity))))
+ #_(js/console.log "after lookup" lookup (:db/id entity) attr @cache-conn))
+ #js [result setResult lookup])]
(react/useEffect
- (fn use-query-effect []
- (let [key (rand)]
- (d/listen! conn key listener)
- #(d/unlisten! conn key)))
- #js [query args])
- [result]))
+ (fn []
+ (swap! cache-conn hbc/assoc-q query reactive-lookup-uid
+ (fn change-handler []
+ (let [q-r (try-hook "useQuery" #(apply hbjs/q query db-conn args))]
+ (when (not= q-r @q-result)
+ (reset! q-result q-r)
+ (setResult (hbjs/ids->entities @db-conn @q-result))))))
+ #(swap! cache-conn hbc/dissoc-q query reactive-lookup-uid))
+ #js [])
+ #js [result]))
+
+;; (defn ^:export useEntity [lookup]
+;; (let [{:strs [db-conn]} (js->clj (react/useContext homebase-context))
+;; cached-entities (react/useMemo #(atom {}) #js [])
+;; run-lookup (react/useCallback
+;; (fn run-lookup []
+;; (touch-entity-cache
+;; (try-hook "useEntity" #(hbjs/entity db-conn lookup))
+;; cached-entities))
+;; #js [lookup])
+;; [result setResult] (react/useState (run-lookup))
+;; listener (react/useCallback
+;; (fn entity-listener []
+;; (let [result (run-lookup)]
+;; (when (changed? #js [result] @cached-entities false)
+;; (setResult result))))
+;; #js [run-lookup])]
+;; (react/useEffect
+;; (fn use-entity-effect []
+;; (let [key (rand)]
+;; (d/listen! db-conn key listener)
+;; #(d/unlisten! db-conn key)))
+;; #js [lookup])
+;; #js [result]))
+
+;; (defn ^:export useQuery [query & args]
+;; (let [{:strs [db-conn]} (js->clj (react/useContext homebase-context))
+;; cached-entities (react/useMemo #(atom {}) #js [])
+;; run-query (react/useCallback
+;; (fn run-query []
+;; (let [result (try-hook "useQuery" #(apply hbjs/q query db-conn args))]
+;; (when (and (not= (count result) (count @cached-entities))
+;; (not= 0 (count result)))
+;; (reset! cached-entities {}))
+;; (.map result (fn [e] (touch-entity-cache e cached-entities)))))
+;; #js [query args])
+;; [result setResult] (react/useState (run-query))
+;; listener (react/useCallback
+;; (fn query-listener []
+;; (let [result (run-query)]
+;; (when (changed? result @cached-entities true)
+;; (setResult result))))
+;; #js [run-query])]
+;; (react/useEffect
+;; (fn use-query-effect []
+;; (let [key (rand)]
+;; (d/listen! db-conn key listener)
+;; #(d/unlisten! db-conn key)))
+;; #js [query args])
+;; #js [result]))
(defn ^:export useTransact []
- (let [conn (react/useContext homebase-context)
- transact (react/useCallback
- (fn transact [tx] (try-hook "useTransact" #(hbjs/transact! conn tx)))
+ (let [{:strs [db-conn]} (js->clj (react/useContext homebase-context))
+ transact (react/useCallback
+ (fn transact [tx] (try-hook "useTransact" #(hbjs/transact! db-conn tx)))
#js [])]
- [transact]))
\ No newline at end of file
+ #js [transact]))
diff --git a/src/homebase/reagent.cljs b/src/homebase/reagent.cljs
new file mode 100644
index 00000000..f697a676
--- /dev/null
+++ b/src/homebase/reagent.cljs
@@ -0,0 +1,119 @@
+(ns homebase.reagent
+ (:require
+ [homebase.cache :as hbc]
+ [datalog-console.chrome.formatters] ; Load the formatters ns to extend cljs-devtools to better render db entities in the chrome console if cljs-devtools is enabled.
+ [devtools.protocols :as dtp :refer [IFormat]]
+ [datascript.impl.entity :as de]
+ [reagent.core :as r]
+ [nano-id.core :refer [nano-id]]
+ [datascript.core :as d]))
+
+(declare lookup-entity)
+
+(deftype Entity [^de/Entity entity meta]
+ IFormat
+ (-header [_] (dtp/-header entity))
+ (-has-body [_] (dtp/-has-body entity))
+ (-body [_] (dtp/-body entity))
+ IMeta
+ (-meta [_] meta)
+ IWithMeta
+ (-with-meta [_ new-meta] (Entity. entity new-meta))
+ ILookup
+ (-lookup [this attr] (lookup-entity this attr nil))
+ (-lookup [this attr not-found] (lookup-entity this attr not-found))
+ IAssociative
+ (-contains-key? [this k] (not= ::nf (lookup-entity this k ::nf)))
+ IFn
+ (-invoke [this k] (lookup-entity this k nil))
+ (-invoke [this k not-found] (lookup-entity this k not-found)))
+
+(defn lookup-entity [^Entity entity attr not-found]
+ (let [result (de/lookup-entity ^de/Entity (.-entity entity) attr not-found)
+ after-lookup (::after-lookup (meta entity))]
+ (when after-lookup (after-lookup {:entity (.-entity entity) :attr attr :result result}))
+ (cond
+ (instance? de/Entity result)
+ (Entity. result {::after-lookup after-lookup})
+
+ (and (set? result) (instance? de/Entity (first result)))
+ (set (map #(Entity. % {::after-lookup after-lookup}) result))
+
+ :else result)))
+
+(defn connect!
+ "Connects a db-conn to a homebase.cache. This is a prerequisite for any of the db read functions in this namespace to be reactive. Returns a homebase.cache connection."
+ [db-conn]
+ (let [cache-conn (hbc/create-conn)]
+ (hbc/connect! cache-conn db-conn)
+ {:cache-conn cache-conn}))
+
+(defn disconnect! [db-conn]
+ (hbc/disconnect! db-conn))
+
+(defn get-cache-conn-from-db [db]
+ (let [cache-conn (:homebase.cache/conn (meta db))
+ _ (when (not cache-conn)
+ (throw (ex-info "Cache not connected. Connect your db to the cache with (homebase.reagent/connect! db-conn) first."
+ {})))]
+ cache-conn))
+
+(defn make-reactive-entity [{:keys [^de/Entity entity r-entity tracked-ea-pairs db-conn cache-conn reactive-lookup-uid] :as args}]
+ (let [top-level-entity-id (:db/id entity)
+ e (Entity. entity {::after-lookup
+ (fn after-lookup [{:keys [^de/Entity entity attr]}]
+ (swap! tracked-ea-pairs conj [(:db/id entity) attr])
+ (swap! cache-conn hbc/assoc-ea [(:db/id entity) attr] reactive-lookup-uid
+ (fn change-handler []
+ (reset! r-entity
+ (make-reactive-entity
+ (merge args {:entity (d/entity @db-conn top-level-entity-id)})))))
+ #_(js/console.log top-level-entity-id (:db/id entity) attr @cache-conn))})]
+ e))
+
+(defn entity
+ "Returns a reactive homebase.reagent/Entity wrapped in a vector.
+
+ It offers a normalized subset of other entity APIs with the
+ primary addition being that implemented protocols are reactive
+ and trigger re-renders when related datoms change.
+
+ NOTE: This takes a conn, not a db."
+ [db-conn lookup]
+ (let [cache-conn (get-cache-conn-from-db @db-conn)
+ entity (d/entity @db-conn lookup)
+ reactive-lookup-uid (nano-id)
+ tracked-ea-pairs (atom #{})
+ r-entity (r/atom nil)
+ hbr-entity (make-reactive-entity {:entity entity :r-entity r-entity :tracked-ea-pairs tracked-ea-pairs :db-conn db-conn :cache-conn cache-conn :reactive-lookup-uid reactive-lookup-uid})
+ _ (reset! r-entity hbr-entity)
+ f (fn []
+ (r/with-let []
+ @r-entity
+ (finally ; handle unmounting this component
+ (doseq [ea @tracked-ea-pairs]
+ (swap! cache-conn hbc/dissoc-ea ea reactive-lookup-uid)
+ #_(js/console.log ea @cache-conn)))))]
+ [(r/track f)]))
+
+(defn q
+ "Returns a reactive query result wrapped in a vector.
+
+ It will trigger a re-render when its result changes.
+
+ NOTE: This takes a conn, not a db."
+ [query db-conn & inputs]
+ (let [cache-conn (get-cache-conn-from-db @db-conn)
+ result (apply d/q query @db-conn inputs)
+ r-result (r/atom result)
+ reactive-lookup-uid (nano-id)
+ _ (swap! cache-conn hbc/assoc-q query reactive-lookup-uid
+ (fn []
+ (reset! r-result (apply d/q query @db-conn inputs))))
+ f (fn []
+ (r/with-let []
+ @r-result
+ (finally ; handle unmounting this component
+ (swap! cache-conn hbc/dissoc-q query reactive-lookup-uid)
+ #_(js/console.log query @cache-conn))))]
+ [(r/track f)]))
\ No newline at end of file
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy