react.cljs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. (ns frontend.db.react
  2. "Transact the tx with some specified relationship so that the components will
  3. be refreshed when subscribed data changed.
  4. It'll be great if we can find an automatically resolving and performant
  5. solution.
  6. "
  7. (:require [datascript.core :as d]
  8. [frontend.date :as date]
  9. [frontend.db.conn :as conn]
  10. [frontend.db.utils :as db-utils]
  11. [frontend.state :as state]
  12. [frontend.util :as util :refer [react]]
  13. [cljs.spec.alpha :as s]
  14. [clojure.core.async :as async]
  15. [clojure.set :as set]))
  16. ;;; keywords specs for reactive query, used by `react/q` calls
  17. ;; ::block
  18. ;; pull-block react-query
  19. (s/def ::block (s/tuple #(= ::block %) int?))
  20. ;; ::block-and-children
  21. ;; get block&children react-query
  22. (s/def ::block-and-children (s/tuple #(= ::block-and-children %) uuid?))
  23. ;; ::journals
  24. ;; get journal-list react-query
  25. (s/def ::journals (s/tuple #(= ::journals %)))
  26. ;; ::page<-pages
  27. ;; get PAGES referencing PAGE
  28. (s/def ::page<-pages (s/tuple #(= ::page<-pages %) int?))
  29. ;; ::refs
  30. ;; get BLOCKS referencing PAGE or BLOCK
  31. (s/def ::refs (s/tuple #(= ::refs %) int?))
  32. ;; custom react-query
  33. (s/def ::custom any?)
  34. (s/def ::react-query-keys (s/or :block ::block
  35. :block-and-children ::block-and-children
  36. :journals ::journals
  37. :page<-pages ::page<-pages
  38. :refs ::refs
  39. :custom ::custom))
  40. (s/def ::affected-keys (s/coll-of ::react-query-keys))
  41. ;; Query atom of map of Key ([repo q inputs]) -> atom
  42. ;; TODO: replace with LRUCache, only keep the latest 20 or 50 items?
  43. (defonce query-state (atom {}))
  44. ;; Current dynamic component
  45. (def ^:dynamic *query-component* nil)
  46. ;; Which reactive queries are triggered by the current component
  47. (def ^:dynamic *reactive-queries* nil)
  48. ;; component -> query-key
  49. (defonce query-components (atom {}))
  50. (defn set-new-result!
  51. [k new-result]
  52. (when-let [result-atom (get-in @query-state [k :result])]
  53. (reset! result-atom new-result)))
  54. (def kv conn/kv)
  55. (defn remove-key!
  56. [repo-url key]
  57. (db-utils/transact! repo-url [[:db.fn/retractEntity [:db/ident key]]])
  58. (set-new-result! [repo-url :kv key] nil))
  59. (defn clear-query-state!
  60. []
  61. (reset! query-state {}))
  62. (defn add-q!
  63. [k query time inputs result-atom transform-fn query-fn inputs-fn]
  64. (let [time' (int (util/safe-parse-float time))] ;; for robustness. `time` should already be float
  65. (swap! query-state assoc k {:query query
  66. :query-time time'
  67. :inputs inputs
  68. :result result-atom
  69. :transform-fn transform-fn
  70. :query-fn query-fn
  71. :inputs-fn inputs-fn}))
  72. result-atom)
  73. (defn remove-q!
  74. [k]
  75. (swap! query-state dissoc k))
  76. (defn add-query-component!
  77. [key component]
  78. (when (and key component)
  79. (swap! query-components update component (fn [col] (set (conj col key))))))
  80. (defn remove-query-component!
  81. [component]
  82. (when-let [queries (get @query-components component)]
  83. (let [all-queries (apply concat (vals @query-components))]
  84. (doseq [query queries]
  85. (let [matched-queries (filter #(= query %) all-queries)]
  86. (when (= 1 (count matched-queries))
  87. (remove-q! query))))))
  88. (swap! query-components dissoc component))
  89. ;; TODO: rename :custom to :query/custom
  90. (defn remove-custom-query!
  91. [repo query]
  92. (remove-q! [repo :custom query]))
  93. ;; Reactive query
  94. (defn get-query-cached-result
  95. [k]
  96. (when-let [result (get @query-state k)]
  97. (when (satisfies? IWithMeta @(:result result))
  98. (set! (.-state (:result result))
  99. (with-meta @(:result result) {:query-time (:query-time result)})))
  100. (:result result)))
  101. (defn q
  102. [repo k {:keys [use-cache? transform-fn query-fn inputs-fn disable-reactive?]
  103. :or {use-cache? true
  104. transform-fn identity}} query & inputs]
  105. {:pre [(s/valid? ::react-query-keys k)]}
  106. (let [kv? (and (vector? k) (= :kv (first k)))
  107. origin-key k
  108. k (vec (cons repo k))]
  109. (when-let [db (conn/get-db repo)]
  110. (let [result-atom (get-query-cached-result k)]
  111. (when-let [component *query-component*]
  112. (add-query-component! k component))
  113. (when-let [queries *reactive-queries*]
  114. (swap! queries conj origin-key))
  115. (if (and use-cache? result-atom)
  116. result-atom
  117. (let [{:keys [result time]} (util/with-time
  118. (-> (cond
  119. query-fn
  120. (query-fn db nil)
  121. inputs-fn
  122. (let [inputs (inputs-fn)]
  123. (apply d/q query db inputs))
  124. kv?
  125. (db-utils/entity db (last k))
  126. (seq inputs)
  127. (apply d/q query db inputs)
  128. :else
  129. (d/q query db))
  130. transform-fn))
  131. result-atom (or result-atom (atom nil))]
  132. ;; Don't notify watches now
  133. (set! (.-state result-atom) result)
  134. (if disable-reactive?
  135. result-atom
  136. (add-q! k query time inputs result-atom transform-fn query-fn inputs-fn))))))))
  137. ;; TODO: Extract several parts to handlers
  138. (defn get-current-page
  139. []
  140. (let [match (:route-match @state/state)
  141. route-name (get-in match [:data :name])
  142. page (case route-name
  143. :page
  144. (get-in match [:path-params :name])
  145. :file
  146. (get-in match [:path-params :path])
  147. (date/journal-name))]
  148. (when page
  149. (let [page-name (util/page-name-sanity-lc page)]
  150. (db-utils/entity [:block/name page-name])))))
  151. (defn- get-block-parents
  152. [db id]
  153. (let [get-parent (fn [id] (:db/id (:block/parent (db-utils/entity db id))))]
  154. (loop [result [id]
  155. id id]
  156. (if-let [parent (get-parent id)]
  157. (recur (conj result parent) parent)
  158. result))))
  159. (defn- get-blocks-parents-from-both-dbs
  160. [db-after db-before block-entities]
  161. (let [current-db-parent-ids (->> (set (keep :block/parent block-entities))
  162. (mapcat (fn [parent]
  163. (get-block-parents db-after (:db/id parent)))))
  164. before-db-parent-ids (->> (map :db/id block-entities)
  165. (mapcat (fn [id]
  166. (get-block-parents db-before id))))]
  167. (set (concat current-db-parent-ids before-db-parent-ids))))
  168. (defn get-affected-queries-keys
  169. "Get affected queries through transaction datoms."
  170. [{:keys [tx-data db-before db-after]}]
  171. {:post [(s/valid? ::affected-keys %)]}
  172. (let [blocks (->> (filter (fn [datom] (contains? #{:block/left :block/parent :block/page} (:a datom))) tx-data)
  173. (map :v)
  174. (distinct))
  175. refs (->> (filter (fn [datom]
  176. (when (contains? #{:block/refs :block/path-refs} (:a datom))
  177. (not= (:v datom)
  178. (:db/id (:block/page (db-utils/entity (:e datom))))))) tx-data)
  179. (map :v)
  180. (distinct))
  181. other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
  182. (map :e))
  183. blocks (-> (concat blocks other-blocks) distinct)
  184. block-entities (keep (fn [block-id]
  185. (let [block-id (if (and (string? block-id) (util/uuid-string? block-id))
  186. [:block/uuid block-id]
  187. block-id)]
  188. (db-utils/entity block-id))) blocks)
  189. affected-keys (concat
  190. (mapcat
  191. (fn [block]
  192. (let [page-id (or
  193. (when (:block/name block) (:db/id block))
  194. (:db/id (:block/page block)))
  195. blocks [[::block (:db/id block)]
  196. [::block (:db/id (:block/parent block))]]
  197. path-refs (:block/path-refs block)
  198. path-refs' (->> (keep (fn [ref]
  199. (when-not (= (:db/id ref) page-id)
  200. [[::refs (:db/id ref)]
  201. [::block (:db/id ref)]])) path-refs)
  202. (apply concat))]
  203. (concat blocks path-refs')))
  204. block-entities)
  205. (mapcat
  206. (fn [ref]
  207. [[::refs ref]
  208. [::block ref]])
  209. refs)
  210. (when-let [current-page-id (:db/id (get-current-page))]
  211. [[::page<-pages current-page-id]]))
  212. parent-ids (get-blocks-parents-from-both-dbs db-after db-before block-entities)
  213. block-children-keys (->>
  214. (keys @query-state)
  215. (keep (fn [ks]
  216. (when (and (= ::block-and-children (second ks))
  217. (contains? parent-ids (last ks)))
  218. (vec (rest ks))))))]
  219. (->>
  220. (util/concat-without-nil
  221. affected-keys
  222. block-children-keys)
  223. set)))
  224. (defn- execute-query!
  225. [graph db k {:keys [query _query-time inputs transform-fn query-fn inputs-fn result]}
  226. {:keys [_skip-query-time-check?]}]
  227. ;; FIXME:
  228. (when true
  229. ;; (or skip-query-time-check?
  230. ;; (<= (or query-time 0) 80))
  231. (let [new-result (->
  232. (cond
  233. query-fn
  234. (let [result (query-fn db result)]
  235. (if (coll? result)
  236. (doall result)
  237. result))
  238. inputs-fn
  239. (let [inputs (inputs-fn)]
  240. (apply d/q query db inputs))
  241. (keyword? query)
  242. (db-utils/get-key-value graph query)
  243. (seq inputs)
  244. (apply d/q query db inputs)
  245. :else
  246. (d/q query db))
  247. transform-fn)]
  248. (when-not (= new-result result)
  249. (set-new-result! k new-result)))))
  250. (defn path-refs-need-recalculated?
  251. [tx-meta]
  252. (when-let [outliner-op (:outliner-op tx-meta)]
  253. (not (or
  254. (contains? #{:collapse-expand-blocks :delete-blocks} outliner-op)
  255. (:undo? tx-meta) (:redo? tx-meta)))))
  256. (defn- refresh-affected-queries!
  257. [repo-url affected-keys]
  258. (let [db (conn/get-db repo-url)]
  259. (doseq [[k cache] @query-state]
  260. (let [custom? (= :custom (second k))
  261. kv? (= :kv (second k))]
  262. (when (and
  263. (= (first k) repo-url)
  264. (or (get affected-keys (vec (rest k)))
  265. custom?
  266. kv?))
  267. (let [{:keys [query query-fn]} cache
  268. {:keys [custom-query?]} (state/edit-in-query-or-refs-component)]
  269. (util/profile
  270. (str "refresh! " (rest k))
  271. (when (or query query-fn)
  272. (try
  273. (let [f #(execute-query! repo-url db k cache {:skip-query-time-check? custom-query?})]
  274. ;; Detects whether user is editing in a custom query, if so, execute the query immediately
  275. (if (and custom? (not custom-query?))
  276. (async/put! (state/get-reactive-custom-queries-chan) [f query])
  277. (f)))
  278. (catch :default e
  279. (js/console.error e)))))))))))
  280. (defn refresh!
  281. "Re-compute corresponding queries (from tx) and refresh the related react components."
  282. [repo-url {:keys [tx-data tx-meta] :as tx}]
  283. (when repo-url
  284. (if (get-in @state/state [:rtc/remote-batch-tx-state repo-url :in-transaction?])
  285. (state/update-state! [:rtc/remote-batch-tx-state repo-url :txs]
  286. (fn [txs]
  287. (conj txs tx)))
  288. (when (and (not (:skip-refresh? tx-meta)) (seq tx-data))
  289. (let [affected-keys (get-affected-queries-keys tx)]
  290. (refresh-affected-queries! repo-url affected-keys))))))
  291. (defn batch-refresh!
  292. [repo-url txs]
  293. (when (and repo-url (seq txs))
  294. (let [affected-keys (apply set/union (map get-affected-queries-keys txs))]
  295. (refresh-affected-queries! repo-url affected-keys)))
  296. (state/set-state! [:rtc/remote-batch-tx-state repo-url]
  297. {:in-transaction? false
  298. :txs []}))
  299. (defn set-key-value
  300. [repo-url key value]
  301. (if value
  302. (db-utils/transact! repo-url [(kv key value)])
  303. (remove-key! repo-url key)))
  304. (defn sub-key-value
  305. ([key]
  306. (sub-key-value (state/get-current-repo) key))
  307. ([repo-url key]
  308. (when (conn/get-db repo-url)
  309. (let [m (some-> (q repo-url [:kv key] {} key key) react)]
  310. (if-let [result (get m key)]
  311. result
  312. m)))))
  313. (defn run-custom-queries-when-idle!
  314. []
  315. (let [chan (state/get-reactive-custom-queries-chan)]
  316. (async/go-loop []
  317. (let [[f query] (async/<! chan)]
  318. (try
  319. (if (state/input-idle? (state/get-current-repo))
  320. (f)
  321. (do
  322. (async/<! (async/timeout 2000))
  323. (async/put! chan [f query])))
  324. (catch :default error
  325. (let [type :custom-query/failed]
  326. (js/console.error (str type "\n" query))
  327. (js/console.error error)))))
  328. (recur))
  329. chan))
  330. (comment
  331. (defn db-graph?
  332. "Whether the current graph is db-only"
  333. []
  334. (= "db" (:db/type (db-utils/entity :db/type)))))