react.cljs 15 KB

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