Files
logseq/src/main/frontend/db/react.cljs
2022-01-27 14:11:59 -05:00

358 lines
12 KiB
Clojure

(ns frontend.db.react
"Transact the tx with some specified relationship so that the components will
be refreshed when subscribed data changed.
It'll be great if we can find an automatically resolving and performant
solution.
"
(:require [clojure.string :as string]
[datascript.core :as d]
[frontend.config :as config]
[frontend.date :as date]
[frontend.db.conn :as conn]
[frontend.db.utils :as db-utils]
[frontend.state :as state]
[frontend.util :as util :refer [react]]
[frontend.util.marker :as marker]))
;; Query atom of map of Key ([repo q inputs]) -> atom
;; TODO: replace with LRUCache, only keep the latest 20 or 50 items?
(defonce query-state (atom {}))
(def ^:dynamic *query-component*)
;; key -> components
(defonce query-components (atom {}))
(defn set-new-result!
[k new-result]
(when-let [result-atom (get-in @query-state [k :result])]
(reset! result-atom new-result)))
;; KV
(defn kv
[key value]
{:db/id -1
:db/ident key
key value})
(defn remove-key!
[repo-url key]
(db-utils/transact! repo-url [[:db.fn/retractEntity [:db/ident key]]])
(set-new-result! [repo-url :kv key] nil))
(defn clear-query-state!
[]
(reset! query-state {}))
(defn clear-query-state-without-refs-and-embeds!
[]
(let [state @query-state
state (->> (filter (fn [[[_repo k] _v]]
(contains? #{:blocks :block/block :custom} k)) state)
(into {}))]
(reset! query-state state)))
(defn get-current-repo-refs-keys
[{:keys [data]}]
(when-let [current-repo (state/get-current-repo)]
(->>
(map (fn [[repo k id]]
(when (and (= repo current-repo)
(contains? #{:block/refed-blocks :block/unlinked-refs} k))
(if (= k :block/refed-blocks)
(if (every? (fn [m]
(when (map? m)
(= id (:db/id (:block/page m))))) data)
nil
[k id])
[k id])))
(keys @query-state))
(remove nil?))))
;; TODO: Add components which subscribed to a specific query
(defn add-q!
[k query inputs result-atom transform-fn query-fn inputs-fn]
(swap! query-state assoc k {:query query
:inputs inputs
:result result-atom
:transform-fn transform-fn
:query-fn query-fn
:inputs-fn inputs-fn})
result-atom)
(defn remove-q!
[k]
(swap! query-state dissoc k))
(defn add-query-component!
[key component]
(swap! query-components update key
(fn [components]
(distinct (conj components component)))))
(defn remove-query-component!
[component]
(reset!
query-components
(->> (for [[k components] @query-components
:let [new-components (remove #(= component %) components)]]
(if (empty? new-components) ; no subscribed components
(do (remove-q! k)
nil)
[k new-components]))
(keep identity)
(into {}))))
(defn get-page-blocks-cache-atom
[repo page-id]
(:result (get @query-state [repo :page/blocks page-id])))
(defn get-block-blocks-cache-atom
[repo block-id]
(:result (get @query-state [repo :block/block block-id])))
;; TODO: rename :custom to :query/custom
(defn remove-custom-query!
[repo query]
(remove-q! [repo :custom query]))
;; Reactive query
(defn query-entity-in-component
([id-or-lookup-ref]
(db-utils/entity (state/get-current-repo) id-or-lookup-ref))
([repo id-or-lookup-ref]
(let [k [:entity id-or-lookup-ref]
result-atom (:result (get @query-state k))]
(when-let [component *query-component*]
(add-query-component! k component))
(when-let [db (conn/get-conn repo)]
(let [result (d/entity db id-or-lookup-ref)
result-atom (or result-atom (atom nil))]
(set! (.-state result-atom) result)
(add-q! k nil nil result-atom identity identity identity))))))
(defn q
[repo k {:keys [use-cache? transform-fn query-fn inputs-fn disable-reactive?]
:or {use-cache? true
transform-fn identity}} query & inputs]
(let [kv? (and (vector? k) (= :kv (first k)))
k (vec (cons repo k))]
(when-let [conn (conn/get-conn repo)]
(let [result-atom (:result (get @query-state k))]
(when-let [component *query-component*]
(add-query-component! k component))
(if (and use-cache? result-atom)
result-atom
(let [result (cond
query-fn
(query-fn conn)
inputs-fn
(let [inputs (inputs-fn)]
(apply d/q query conn inputs))
kv?
(d/entity conn (last k))
(seq inputs)
(apply d/q query conn inputs)
:else
(d/q query conn))
result (transform-fn result)
result-atom (or result-atom (atom nil))]
;; Don't notify watches now
(set! (.-state result-atom) result)
(if-not disable-reactive?
(add-q! k query inputs result-atom transform-fn query-fn inputs-fn)
result-atom)))))))
;; TODO: Extract several parts to handlers
(defn get-current-page
[]
(let [match (:route-match @state/state)
route-name (get-in match [:data :name])
page (case route-name
:page
(get-in match [:path-params :name])
:file
(get-in match [:path-params :path])
(date/journal-name))]
(when page
(let [page-name (util/page-name-sanity-lc page)]
(db-utils/entity [:block/name page-name])))))
(defn get-current-priority
[]
(let [match (:route-match @state/state)
route-name (get-in match [:data :name])]
(when (= route-name :page)
(when-let [page-name (get-in match [:path-params :name])]
(and (contains? #{"a" "b" "c"} (string/lower-case page-name))
(string/upper-case page-name))))))
(defn get-current-marker
[]
(let [match (:route-match @state/state)
route-name (get-in match [:data :name])]
(when (= route-name :page)
(when-let [page-name (get-in match [:path-params :name])]
(and (marker/marker? page-name)
(string/upper-case page-name))))))
(defn get-related-keys
[{:keys [key data]}]
(cond
(coll? key)
[key]
:else
(case key
(:block/change :block/insert)
(when-let [blocks (seq data)]
(let [pre-block? (:block/pre-block? (first blocks))
current-priority (get-current-priority)
current-marker (get-current-marker)
current-page-id (:db/id (get-current-page))
related-keys (->>
(util/concat-without-nil
(mapcat
(fn [block]
(when-let [page-id (or (:db/id (:block/page block))
(and (int? (:block/page block))
(:block/page block)))]
[[:blocks (:block/uuid block)]
[:page/blocks page-id]
[:page/ref-pages page-id]]))
blocks)
(when pre-block?
[[:contents]
[:page/published]])
;; affected priority
(when current-priority
[[:priority/blocks current-priority]])
(when current-marker
[[:marker/blocks current-marker]])
(when current-page-id
[[:page/ref-pages current-page-id]
[:page/mentioned-pages current-page-id]])
(apply concat
(for [{:block/keys [refs]} blocks]
(map (fn [ref]
(cond
(and (map? ref) (:block/name ref))
[:page/blocks (:db/id (db-utils/entity [:block/name (:block/name ref)]))]
(and (vector? ref) (= (first ref) :block/uuid))
[:block/refs-count (second ref)]
:else
nil))
refs))))
(distinct))
refed-pages (map
(fn [[k page-id]]
(when (= k :block/refed-blocks)
[:page/ref-pages page-id]))
related-keys)
all-refed-blocks (get-current-repo-refs-keys {:key key
:data data})
custom-queries (some->>
(filter (fn [v]
(and (= (first v) (state/get-current-repo))
(= (second v) :custom)))
(keys @query-state))
(map (fn [v]
(vec (drop 1 v)))))
block-blocks (some->>
(filter (fn [v]
(and (= (first v) (state/get-current-repo))
(= (second v) :block/block)))
(keys @query-state))
(map (fn [v]
(vec (drop 1 v)))))]
(->>
(util/concat-without-nil
related-keys
refed-pages
all-refed-blocks
custom-queries
block-blocks)
distinct)))
[[key]])))
(defn refresh!
[repo-url handler-opts]
(let [related-keys (get-related-keys handler-opts)
db (conn/get-conn repo-url)]
(doseq [related-key related-keys]
(let [related-key (vec (cons repo-url related-key))]
(when-let [cache (get @query-state related-key)]
(let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
(when (or query query-fn)
(let [new-result (->
(cond
query-fn
(let [result (query-fn db)]
(if (coll? result)
(doall result)
result))
inputs-fn
(let [inputs (inputs-fn)]
(apply d/q query db inputs))
(keyword? query)
(db-utils/get-key-value repo-url query)
(seq inputs)
(apply d/q query db inputs)
:else
(d/q query db))
transform-fn)]
(set-new-result! related-key new-result)))))))))
(defn transact-react!
[repo-url tx-data handler-opts]
(when-not config/publishing?
(let [repo-url (or repo-url (state/get-current-repo))
tx-data (->> (util/remove-nils tx-data)
(remove nil?))
get-conn (fn [] (conn/get-conn repo-url false))]
(when (and (seq tx-data) (get-conn))
(d/transact! (get-conn) (vec tx-data))
(refresh! repo-url handler-opts)))))
(defn set-key-value
[repo-url key value]
(if value
(transact-react! repo-url [(kv key value)]
{:key [:kv key]})
(remove-key! repo-url key)))
(defn sub-key-value
([key]
(sub-key-value (state/get-current-repo) key))
([repo-url key]
(when (conn/get-conn repo-url)
(let [m (some-> (q repo-url [:kv key] {} key key) react)]
(if-let [result (get m key)]
result
m)))))