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 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