From a08fabe8f6ba5a02e1ceee7b79229801db7c6815 Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Mon, 7 Jun 2021 08:19:15 -0700 Subject: [PATCH 1/7] feat(reagent): add reagent integration --- deps.edn | 3 +- src/dev/example/core.cljs | 10 +- src/dev/example/{ => react}/array.cljs | 4 +- src/dev/example/{ => react}/counter.cljs | 4 +- src/dev/example/{ => react}/todo.cljs | 4 +- .../example/{ => react}/todo_firebase.cljs | 4 +- src/dev/example/reagent/counter.cljs | 23 ++++ src/dev/example/reagent/todo.cljs | 71 ++++++++++++ src/homebase/cache.cljs | 80 +++++++++++++ src/homebase/reagent.cljs | 109 ++++++++++++++++++ 10 files changed, 299 insertions(+), 13 deletions(-) rename src/dev/example/{ => react}/array.cljs (89%) rename src/dev/example/{ => react}/counter.cljs (89%) rename src/dev/example/{ => react}/todo.cljs (91%) rename src/dev/example/{ => react}/todo_firebase.cljs (88%) create mode 100644 src/dev/example/reagent/counter.cljs create mode 100644 src/dev/example/reagent/todo.cljs create mode 100644 src/homebase/cache.cljs create mode 100644 src/homebase/reagent.cljs diff --git a/deps.edn b/deps.edn index dc208ad8..1891dd5c 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 "91d5b6009d66807ceec9807a1f8ed099a0a6f219"} ;; 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/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..17065f6e --- /dev/null +++ b/src/dev/example/reagent/counter.cljs @@ -0,0 +1,23 @@ +(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]])) + +(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) \ 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..5a103f85 --- /dev/null +++ b/src/dev/example/reagent/todo.cljs @@ -0,0 +1,71 @@ +(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]])) + +(def db-conn (d/create-conn {})) +(hbr/connect! db-conn) +(d/transact! db-conn [{:todo/name "Do another thing" + :todo/created-at (js/Date.now)} + {:todo/name "Do a thing" + :todo/created-at (js/Date.now)}]) + +#_(dotimes [n 1000] + (d/transact! db-conn [{:todo/name (str n) + :todo/created-at (js/Date.now)}])) + +(defn todo [id] + (let [todo (hbr/entity db-conn id)] + (fn [] + [:div {:style {:display "flex" :flex-direction "row" :align-items "center"}} + [:input + {:type "checkbox" + :checked (true? (:todo/completed? @todo)) + :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}] + [:div {:style {:padding-left 6}} + [:input + {:type "text" + :style {:border "none" :width "auto" :font-weight "bold"} + :value (:todo/name @todo) + :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/name (goog.object/getValueByKeys % #js ["target" "value"])]])}] + [:small {:style {:padding-left 6}} + (.toString (js/Date. (:todo/created-at @todo)))] + [:button + {:on-click #(d/transact! db-conn [[:db/retractEntity (:db/id @todo)]])} + "Delete"]]]))) + +(defn todos [] + (let [todos (hbr/q '[:find ?e ?t + :where [?e :todo/created-at ?t]] + db-conn)] + (fn [] + [:div + (doall + (for [[id] (reverse (sort-by peek @todos))] + ^{:key id} [todo id]))]))) + +(defn new-todo [] + (let [name (r/atom "")] + (fn [] + [:form {:on-submit (fn [e] + (.preventDefault e) + (d/transact! db-conn [{:todo/name @name + :todo/created-at (js/Date.now)}]) + (reset! name ""))} + [: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 [] + [:div + [new-todo] + [todos]]) + +(defcard-rg todo-example + todo-app) \ No newline at end of file diff --git a/src/homebase/cache.cljs b/src/homebase/cache.cljs new file mode 100644 index 00000000..9ab57525 --- /dev/null +++ b/src/homebase/cache.cljs @@ -0,0 +1,80 @@ +(ns homebase.cache + (:require + [datascript.core :as datascript] + [datascript.db])) + +(defn create-conn [] + (atom + {:ea {} + :q {}})) + +(defn assoc-ea + [cache eid-attr-tuple component-uid change-handler] + (assoc-in cache [:ea eid-attr-tuple component-uid] change-handler)) + +(defn dissoc-ea + [cache eid-attr-tuple component-uid] + (let [cache (update-in cache [:ea eid-attr-tuple] dissoc component-uid)] + (if (empty? (get-in cache [:ea eid-attr-tuple])) + (update cache :ea dissoc eid-attr-tuple) + cache))) + +(defn assoc-q + [cache query component-uid change-handler] + (assoc-in cache [:q query component-uid] change-handler)) + +(defn dissoc-q + [cache query component-uid] + (let [cache (update-in cache [:q query] dissoc component-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] + ;; EA handlers + (doseq [[e a :as datom] tx-data] + (let [subscriptions (get-in cache [:ea [e a]])] + (doseq [[component-uid change-handler] subscriptions] + (change-handler {:datom datom + :component-uid component-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 (map (comp flatten seq) (vals (:q cache)))] + (doseq [[component-uid change-handler] subscriptions] + (change-handler {:component-uid component-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/reagent.cljs b/src/homebase/reagent.cljs new file mode 100644 index 00000000..0a57bab9 --- /dev/null +++ b/src/homebase/reagent.cljs @@ -0,0 +1,109 @@ +(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 :attr attr :result result})) + (if (instance? de/Entity result) + (Entity. result {::after-lookup after-lookup}) + 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)) + +(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 component-uid] :as args}] + (let [entity-id (:db/id entity) + e (Entity. entity {::after-lookup + (fn [{:keys [attr]}] + (swap! tracked-ea-pairs conj [entity-id attr]) + (swap! cache-conn hbc/assoc-ea [entity-id attr] component-uid + (fn [] + (reset! r-entity + (make-reactive-entity + (merge args {:entity (d/entity @db-conn entity-id)}))))))})] + e)) + +(defn entity + "Returns a reactive homebase.reagent/Entity. + + 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) + component-uid (nano-id) + tracked-ea-pairs (atom #{}) + r-entity (r/atom entity) + 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 :component-uid component-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 component-uid) + #_(js/console.log ea @cache-conn)))))] + (r/track f))) + +(defn q + "Returns a reactive query result that 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) + component-uid (nano-id) + _ (swap! cache-conn hbc/assoc-q query component-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 component-uid) + #_(js/console.log query @cache-conn))))] + (r/track f))) \ No newline at end of file From 4622e99b15fa33a6b63ab7e5b186a5d03ec64867 Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Wed, 9 Jun 2021 17:01:52 -0700 Subject: [PATCH 2/7] fix(cache): handle refs reverse refs and queries better --- src/dev/example/reagent/counter.cljs | 14 +-- src/dev/example/reagent/todo.cljs | 175 ++++++++++++++++++++++----- src/homebase/cache.cljs | 36 +++--- src/homebase/reagent.cljs | 52 ++++---- 4 files changed, 201 insertions(+), 76 deletions(-) diff --git a/src/dev/example/reagent/counter.cljs b/src/dev/example/reagent/counter.cljs index 17065f6e..1ccd2d70 100644 --- a/src/dev/example/reagent/counter.cljs +++ b/src/dev/example/reagent/counter.cljs @@ -11,13 +11,13 @@ (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"]]])) + (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) \ No newline at end of file diff --git a/src/dev/example/reagent/todo.cljs b/src/dev/example/reagent/todo.cljs index 5a103f85..0d9b7887 100644 --- a/src/dev/example/reagent/todo.cljs +++ b/src/dev/example/reagent/todo.cljs @@ -7,55 +7,164 @@ (:require-macros [devcards.core :refer [defcard-rg]])) -(def db-conn (d/create-conn {})) +(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 "Do another thing" - :todo/created-at (js/Date.now)} - {:todo/name "Do a thing" - :todo/created-at (js/Date.now)}]) +(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)] + (let [[todo] (hbr/entity db-conn id)] (fn [] - [:div {:style {:display "flex" :flex-direction "row" :align-items "center"}} - [:input - {:type "checkbox" - :checked (true? (:todo/completed? @todo)) - :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}] - [:div {:style {:padding-left 6}} + [: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 {:border "none" :width "auto" :font-weight "bold"} + :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"])]])}] - [:small {:style {:padding-left 6}} - (.toString (js/Date. (:todo/created-at @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"]]]))) + "Delete"]] + [:div + [:small {:style {:color "grey"}} + (.toLocaleString (js/Date. (:todo/created-at @todo)))]]]))) -(defn todos [] - (let [todos (hbr/q '[:find ?e ?t - :where [?e :todo/created-at ?t]] - db-conn)] - (fn [] - [:div - (doall - (for [[id] (reverse (sort-by peek @todos))] - ^{:key id} [todo id]))]))) +(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 [] +(defn new-todo [filters] (let [name (r/atom "")] - (fn [] + (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! name "") + (reset! filters default-filters))} [:input {:type "text" :on-change #(reset! name (goog.object/getValueByKeys % #js ["target" "value"])) :value @name @@ -63,9 +172,11 @@ [:button {:type "submit"} "Create todo"]]))) (defn todo-app [] - [:div - [new-todo] - [todos]]) + (let [filters (r/atom default-filters)] + (fn [] + [:div + [new-todo filters] + [todos filters]]))) (defcard-rg todo-example todo-app) \ No newline at end of file diff --git a/src/homebase/cache.cljs b/src/homebase/cache.cljs index 9ab57525..560224e8 100644 --- a/src/homebase/cache.cljs +++ b/src/homebase/cache.cljs @@ -9,44 +9,48 @@ :q {}})) (defn assoc-ea - [cache eid-attr-tuple component-uid change-handler] - (assoc-in cache [:ea eid-attr-tuple component-uid] change-handler)) + [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 component-uid] - (let [cache (update-in cache [:ea eid-attr-tuple] dissoc component-uid)] + [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 component-uid change-handler] - (assoc-in cache [:q query component-uid] change-handler)) + [cache query reactive-lookup-uid change-handler] + (assoc-in cache [:q query reactive-lookup-uid] change-handler)) (defn dissoc-q - [cache query component-uid] - (let [cache (update-in cache [:q query] dissoc component-uid)] + [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 +(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] + (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 [[component-uid change-handler] subscriptions] - (change-handler {:datom datom - :component-uid component-uid})))) + (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 (map (comp flatten seq) (vals (:q cache)))] - (doseq [[component-uid change-handler] subscriptions] - (change-handler {:component-uid component-uid})))))) + (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) diff --git a/src/homebase/reagent.cljs b/src/homebase/reagent.cljs index 0a57bab9..70041a76 100644 --- a/src/homebase/reagent.cljs +++ b/src/homebase/reagent.cljs @@ -31,17 +31,22 @@ (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 :attr attr :result result})) - (if (instance? de/Entity result) + (when after-lookup (after-lookup {:entity (.-entity entity) :attr attr :result result})) + (cond + (instance? de/Entity result) (Entity. result {::after-lookup after-lookup}) - result))) + + (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 cache-conn})) (defn disconnect! [db-conn] (hbc/disconnect! db-conn)) @@ -53,20 +58,21 @@ {})))] cache-conn)) -(defn make-reactive-entity [{:keys [^de/Entity entity r-entity tracked-ea-pairs db-conn cache-conn component-uid] :as args}] - (let [entity-id (:db/id entity) +(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 [{:keys [attr]}] - (swap! tracked-ea-pairs conj [entity-id attr]) - (swap! cache-conn hbc/assoc-ea [entity-id attr] component-uid + (fn [{:keys [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 [] (reset! r-entity (make-reactive-entity - (merge args {:entity (d/entity @db-conn entity-id)}))))))})] + (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. + "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 @@ -76,34 +82,38 @@ [db-conn lookup] (let [cache-conn (get-cache-conn-from-db @db-conn) entity (d/entity @db-conn lookup) - component-uid (nano-id) + reactive-lookup-uid (nano-id) tracked-ea-pairs (atom #{}) - r-entity (r/atom entity) - 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 :component-uid component-uid}) + 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 component-uid) + (swap! cache-conn hbc/dissoc-ea ea reactive-lookup-uid) #_(js/console.log ea @cache-conn)))))] - (r/track f))) + [(r/track f)])) (defn q - "Returns a reactive query result that will trigger a re-render when its result changes. NOTE: This takes a conn, not a db." + "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) - component-uid (nano-id) - _ (swap! cache-conn hbc/assoc-q query component-uid + 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 component-uid) + (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 + [(r/track f)])) \ No newline at end of file From 605b9c778f18d5a60d1fd416d54e5ca6758fc61c Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Fri, 11 Jun 2021 12:26:39 -0700 Subject: [PATCH 3/7] refactor(react): move useentity to new cache --- deps.edn | 2 +- src/dev/example/js/todo.jsx | 5 +- src/dev/example/js_compiled/todo.js | 2 +- src/dev/example/reagent/counter.cljs | 8 +- src/dev/example/reagent/todo.cljs | 8 +- src/homebase/js.cljs | 40 +++++-- src/homebase/react.cljs | 170 +++++++++++++++++++-------- src/homebase/reagent.cljs | 4 +- 8 files changed, 165 insertions(+), 74 deletions(-) diff --git a/deps.edn b/deps.edn index f0388b80..d0693a7b 100644 --- a/deps.edn +++ b/deps.edn @@ -5,7 +5,7 @@ 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/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/reagent/counter.cljs b/src/dev/example/reagent/counter.cljs index 1ccd2d70..216925de 100644 --- a/src/dev/example/reagent/counter.cljs +++ b/src/dev/example/reagent/counter.cljs @@ -4,7 +4,8 @@ [datascript.core :as d] [homebase.reagent :as hbr]) (:require-macros - [devcards.core :refer [defcard-rg]])) + [devcards.core :refer [defcard-rg defcard-doc]] + [dev.macros :refer [inline-resource]])) (def db-conn (d/create-conn {})) (hbr/connect! db-conn) @@ -20,4 +21,7 @@ "Increment"]]]))) (defcard-rg counter-example - counter) \ No newline at end of file + 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 index 0d9b7887..4cffe860 100644 --- a/src/dev/example/reagent/todo.cljs +++ b/src/dev/example/reagent/todo.cljs @@ -5,7 +5,8 @@ [reagent.core :as r] [homebase.reagent :as hbr]) (:require-macros - [devcards.core :refer [defcard-rg]])) + [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} @@ -179,4 +180,7 @@ [todos filters]]))) (defcard-rg todo-example - todo-app) \ No newline at end of file + 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/js.cljs b/src/homebase/js.cljs index e83f6fb8..6eda01a3 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,8 +347,8 @@ (-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))) diff --git a/src/homebase/react.cljs b/src/homebase/react.cljs index 70dd95b7..75914a87 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) @@ -134,65 +134,83 @@ 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! {:db-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])) - + (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 []) + ;; hbr-entity (make-reactive-entity {:entity entity :setEntity setResult :tracked-ea-pairs tracked-ea-pairs :db-conn db-conn :cache-conn cache-conn :reactive-lookup-uid reactive-lookup-uid}) + [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]) + (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 []) [result])) (defn ^:export useQuery [query & args] - (let [conn (react/useContext homebase-context) + (let [{:strs [db-conn]} (js->clj (react/useContext homebase-context)) cached-entities (react/useMemo #(atom {}) #js []) - run-query (react/useCallback + run-query (react/useCallback (fn run-query [] - (let [result (try-hook "useQuery" #(apply hbjs/q query conn args))] + (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 {})) @@ -208,14 +226,64 @@ (react/useEffect (fn use-query-effect [] (let [key (rand)] - (d/listen! conn key listener) - #(d/unlisten! conn key))) + (d/listen! db-conn key listener) + #(d/unlisten! db-conn key))) #js [query args]) [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]) +;; [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]) +;; [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 diff --git a/src/homebase/reagent.cljs b/src/homebase/reagent.cljs index 70041a76..f697a676 100644 --- a/src/homebase/reagent.cljs +++ b/src/homebase/reagent.cljs @@ -61,10 +61,10 @@ (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 [{:keys [entity attr]}] + (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 [] + (fn change-handler [] (reset! r-entity (make-reactive-entity (merge args {:entity (d/entity @db-conn top-level-entity-id)}))))) From 3913b957acabb7fd2498af42cceec84729602f68 Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Wed, 16 Jun 2021 10:01:51 -0700 Subject: [PATCH 4/7] fix: conn --- src/homebase/react.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/homebase/react.cljs b/src/homebase/react.cljs index 75914a87..b3f07d05 100644 --- a/src/homebase/react.cljs +++ b/src/homebase/react.cljs @@ -146,7 +146,7 @@ (react/useEffect (fn [] (hbc/connect! cache-conn db-conn) - (datalog-console/init! {:db-conn db-conn}) + (datalog-console/init! {:conn db-conn}) (when initial-tx (hbjs/transact! db-conn initial-tx)) (setInitializing? false) #(hbc/disconnect! db-conn)) From 14fa5131e256e5519d2f4cc4344d11457385343f Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Wed, 16 Jun 2021 10:30:16 -0700 Subject: [PATCH 5/7] refactor: wip --- src/homebase/js.cljs | 14 +-- src/homebase/react.cljs | 186 +++++++--------------------------------- 2 files changed, 37 insertions(+), 163 deletions(-) diff --git a/src/homebase/js.cljs b/src/homebase/js.cljs index 6eda01a3..5a193d7c 100644 --- a/src/homebase/js.cljs +++ b/src/homebase/js.cljs @@ -352,12 +352,6 @@ (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] @@ -373,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 b3f07d05..1f629b30 100644 --- a/src/homebase/react.cljs +++ b/src/homebase/react.cljs @@ -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)]) @@ -175,14 +96,13 @@ (listener-fn (datoms->js (:tx-data %)))))) "removeTransactListener" #(d/unlisten! db-conn key)}) #js [])] - [client])) + #js [client])) (defn ^:export useEntity [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 []) - ;; hbr-entity (make-reactive-entity {:entity entity :setEntity setResult :tracked-ea-pairs tracked-ea-pairs :db-conn db-conn :cache-conn cache-conn :reactive-lookup-uid reactive-lookup-uid}) [result setResult] (react/useState hbjs-entity) after-lookup (react/useCallback (fn after-lookup [{:keys [^de/Entity entity attr]}] @@ -203,87 +123,41 @@ (swap! cache-conn hbc/dissoc-ea ea reactive-lookup-uid) #_(js/console.log "dissoc-ea" ea @cache-conn))) #js []) - [result])) + #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])] + (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! db-conn key listener) - #(d/unlisten! db-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]) -;; [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]) -;; [result])) - (defn ^:export useTransact [] (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])) + From c086d759f4692e4ee73128fa248b6ca74181cf56 Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Fri, 11 Jun 2021 12:26:39 -0700 Subject: [PATCH 6/7] refactor(react): move useentity to new cache --- src/homebase/react.cljs | 55 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/homebase/react.cljs b/src/homebase/react.cljs index 1f629b30..67ea0e7e 100644 --- a/src/homebase/react.cljs +++ b/src/homebase/react.cljs @@ -38,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)) @@ -153,11 +153,60 @@ #(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 [{:strs [db-conn]} (js->clj (react/useContext homebase-context)) transact (react/useCallback (fn transact [tx] (try-hook "useTransact" #(hbjs/transact! db-conn tx))) #js [])] #js [transact])) - From faf2f5b2f2cfca773bc4e3db43a78d8f1dc966e1 Mon Sep 17 00:00:00 2001 From: Chris Smothers Date: Wed, 16 Jun 2021 10:30:33 -0700 Subject: [PATCH 7/7] refactor: conn