db_worker.cljs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. (ns frontend.db-worker
  2. "Worker used for browser DB implementation"
  3. (:require [promesa.core :as p]
  4. [datascript.storage :refer [IStorage]]
  5. [clojure.edn :as edn]
  6. [datascript.core :as d]
  7. [logseq.db.sqlite.common-db :as sqlite-common-db]
  8. [shadow.cljs.modern :refer [defclass]]
  9. [datascript.transit :as dt]
  10. ["@logseq/sqlite-wasm" :default sqlite3InitModule]
  11. ["comlink" :as Comlink]
  12. [clojure.string :as string]
  13. [cljs-bean.core :as bean]
  14. [frontend.worker.search :as search]
  15. [logseq.db.sqlite.util :as sqlite-util]
  16. [frontend.worker.state :as state]
  17. [frontend.worker.file :as file]
  18. [logseq.db :as ldb]
  19. [frontend.worker.rtc.op-mem-layer :as op-mem-layer]
  20. [frontend.worker.rtc.db-listener :as rtc-db-listener]
  21. [frontend.worker.rtc.full-upload-download-graph :as rtc-updown]
  22. [frontend.worker.rtc.core :as rtc-core]
  23. [clojure.core.async :as async]
  24. [frontend.worker.async-util :include-macros true :refer [<?]]
  25. [frontend.worker.util :as worker-util]
  26. [frontend.worker.handler.page.rename :as worker-page-rename]))
  27. (defonce *sqlite state/*sqlite)
  28. (defonce *sqlite-conns state/*sqlite-conns)
  29. (defonce *datascript-conns state/*datascript-conns)
  30. (defonce *opfs-pools state/*opfs-pools)
  31. (defn- get-pool-name
  32. [graph-name]
  33. (str "logseq-pool-" (sqlite-common-db/sanitize-db-name graph-name)))
  34. (defn- <get-opfs-pool
  35. [graph]
  36. (or (state/get-opfs-pool graph)
  37. (p/let [^js pool (.installOpfsSAHPoolVfs @*sqlite #js {:name (get-pool-name graph)
  38. :initialCapacity 20})]
  39. (swap! *opfs-pools assoc graph pool)
  40. pool)))
  41. (defn- init-sqlite-module!
  42. []
  43. (when-not @*sqlite
  44. (p/let [electron? (string/includes? (.. js/location -href) "electron=true")
  45. base-url (str js/self.location.protocol "//" js/self.location.host)
  46. sqlite-wasm-url (if electron?
  47. (js/URL. "sqlite3.wasm" (.. js/location -href))
  48. (str base-url (string/replace js/self.location.pathname "db-worker.js" "")))
  49. sqlite (sqlite3InitModule (clj->js {:url sqlite-wasm-url
  50. :print js/console.log
  51. :printErr js/console.error}))]
  52. (reset! *sqlite sqlite)
  53. nil)))
  54. (def repo-path "/db.sqlite")
  55. (defn- <export-db-file
  56. [repo]
  57. (p/let [^js pool (<get-opfs-pool repo)]
  58. (when pool
  59. (.exportFile ^js pool repo-path))))
  60. (defn- <import-db
  61. [^js pool data]
  62. (.importDb ^js pool repo-path data))
  63. (defn upsert-addr-content!
  64. "Upsert addr+data-seq"
  65. [repo data delete-addrs]
  66. (let [^Object db (state/get-sqlite-conn repo)]
  67. (assert (some? db) "sqlite db not exists")
  68. (.transaction db (fn [tx]
  69. (doseq [item data]
  70. (.exec tx #js {:sql "INSERT INTO kvs (addr, content) values ($addr, $content) on conflict(addr) do update set content = $content"
  71. :bind item}))
  72. (doseq [addr delete-addrs]
  73. (.exec db #js {:sql "Delete from kvs where addr = ?"
  74. :bind #js [addr]}))))))
  75. (defn restore-data-from-addr
  76. [repo addr]
  77. (let [^Object db (state/get-sqlite-conn repo)]
  78. (assert (some? db) "sqlite db not exists")
  79. (when-let [content (-> (.exec db #js {:sql "select content from kvs where addr = ?"
  80. :bind #js [addr]
  81. :rowMode "array"})
  82. ffirst)]
  83. (edn/read-string content))))
  84. (defn new-sqlite-storage
  85. [repo _opts]
  86. (reify IStorage
  87. (-store [_ addr+data-seq delete-addrs]
  88. (prn :debug (str "SQLite store addr+data count: " (count addr+data-seq)))
  89. (let [data (map
  90. (fn [[addr data]]
  91. #js {:$addr addr
  92. :$content (pr-str data)})
  93. addr+data-seq)]
  94. ;; async write so that UI can be refreshed earlier
  95. (async/go
  96. (upsert-addr-content! repo data delete-addrs))))
  97. (-restore [_ addr]
  98. (restore-data-from-addr repo addr))))
  99. (defn- close-db-aux!
  100. [repo ^Object db ^Object search]
  101. (swap! *sqlite-conns dissoc repo)
  102. (swap! *datascript-conns dissoc repo)
  103. (when db (.close db))
  104. (when search (.close search))
  105. (when-let [^js pool (state/get-opfs-pool repo)]
  106. (.releaseAccessHandles pool))
  107. (swap! *opfs-pools dissoc repo))
  108. (defn- close-other-dbs!
  109. [repo]
  110. (doseq [[r {:keys [db search]}] @*sqlite-conns]
  111. (when-not (= repo r)
  112. (close-db-aux! r db search))))
  113. (defn- close-db!
  114. [repo]
  115. (let [{:keys [db search]} (@*sqlite-conns repo)]
  116. (close-db-aux! repo db search)))
  117. (defn- create-or-open-db!
  118. [repo]
  119. (when-not (state/get-sqlite-conn repo)
  120. (p/let [^js pool (<get-opfs-pool repo)
  121. capacity (.getCapacity pool)
  122. _ (when (zero? capacity) ; file handle already releases since pool will be initialized only once
  123. (.acquireAccessHandles pool))
  124. db (new (.-OpfsSAHPoolDb pool) repo-path)
  125. search-db (new (.-OpfsSAHPoolDb pool) (str "search" repo-path))
  126. storage (new-sqlite-storage repo {})]
  127. (swap! *sqlite-conns assoc repo {:db db
  128. :search search-db})
  129. (.exec db "PRAGMA locking_mode=exclusive")
  130. (sqlite-common-db/create-kvs-table! db)
  131. (search/create-tables-and-triggers! search-db)
  132. (let [schema (sqlite-util/get-schema repo)
  133. conn (sqlite-common-db/get-storage-conn storage schema)]
  134. (swap! *datascript-conns assoc repo conn)
  135. (p/let [_ (op-mem-layer/<init-load-from-indexeddb! repo)]
  136. (rtc-db-listener/listen-to-db-changes! repo conn))
  137. nil))))
  138. (defn- iter->vec [iter]
  139. (when iter
  140. (p/loop [acc []]
  141. (p/let [elem (.next iter)]
  142. (if (.-done elem)
  143. acc
  144. (p/recur (conj acc (.-value elem))))))))
  145. (defn- <list-all-files
  146. []
  147. (let [dir? #(= (.-kind %) "directory")]
  148. (p/let [^js root (.getDirectory js/navigator.storage)]
  149. (p/loop [result []
  150. dirs [root]]
  151. (if (empty? dirs)
  152. result
  153. (p/let [dir (first dirs)
  154. result (conj result dir)
  155. values-iter (when (dir? dir) (.values dir))
  156. values (when values-iter (iter->vec values-iter))
  157. current-dir-dirs (filter dir? values)
  158. result (concat result values)
  159. dirs (concat
  160. current-dir-dirs
  161. (rest dirs))]
  162. (p/recur result dirs)))))))
  163. (defn- <db-exists?
  164. [graph]
  165. (->
  166. (p/let [^js root (.getDirectory js/navigator.storage)
  167. _dir-handle (.getDirectoryHandle root (str "." (get-pool-name graph)))]
  168. true)
  169. (p/catch
  170. (fn [_e] ; not found
  171. false))))
  172. (defn- remove-vfs!
  173. [^js pool]
  174. (when pool
  175. (.removeVfs ^js pool)))
  176. (defn- get-search-db
  177. [repo]
  178. (state/get-sqlite-conn repo {:search? true}))
  179. #_:clj-kondo/ignore
  180. (defclass DBWorker
  181. (extends js/Object)
  182. (constructor
  183. [this]
  184. (super))
  185. Object
  186. (getVersion
  187. [_this]
  188. (when-let [sqlite @*sqlite]
  189. (.-version sqlite)))
  190. (init
  191. [_this rtc-ws-url]
  192. (reset! state/*rtc-ws-url rtc-ws-url)
  193. (init-sqlite-module!))
  194. (listDB
  195. [_this]
  196. (p/let [all-files (<list-all-files)
  197. dbs (->>
  198. (keep (fn [file]
  199. (when (and
  200. (= (.-kind file) "directory")
  201. (string/starts-with? (.-name file) ".logseq-pool-"))
  202. (-> (.-name file)
  203. (string/replace-first ".logseq-pool-" "")
  204. ;; TODO: DRY
  205. (string/replace "+3A+" ":")
  206. (string/replace "++" "/"))))
  207. all-files)
  208. distinct)]
  209. ;; (prn :debug :all-files (map #(.-name %) all-files))
  210. ;; (prn :debug :all-files-count (count (filter
  211. ;; #(= (.-kind %) "file")
  212. ;; all-files)))
  213. ;; (prn :dbs dbs)
  214. (bean/->js dbs)))
  215. (createOrOpenDB
  216. [_this repo]
  217. (p/let [_ (close-other-dbs! repo)]
  218. (create-or-open-db! repo)))
  219. (getMaxTx
  220. [_this repo]
  221. (when-let [conn (state/get-datascript-conn repo)]
  222. (:max-tx @conn)))
  223. (q [_this repo inputs-str]
  224. "Datascript q"
  225. (when-let [conn (state/get-datascript-conn repo)]
  226. (let [inputs (edn/read-string inputs-str)]
  227. (let [result (apply d/q (first inputs) @conn (rest inputs))]
  228. (bean/->js result)))))
  229. (transact
  230. [_this repo tx-data tx-meta context]
  231. (when repo (state/set-db-latest-tx-time! repo))
  232. (when-let [conn (state/get-datascript-conn repo)]
  233. (try
  234. (let [tx-data (if (string? tx-data)
  235. (edn/read-string tx-data)
  236. tx-data)
  237. tx-meta (if (string? tx-meta)
  238. (edn/read-string tx-meta)
  239. tx-meta)
  240. context (if (string? context)
  241. (edn/read-string context)
  242. context)
  243. _ (when context (state/set-context! context))
  244. tx-meta' (if (:new-graph? tx-meta)
  245. tx-meta
  246. (-> tx-meta
  247. (assoc :skip-store? true) ; delay writes to the disk
  248. (dissoc :insert-blocks?)))]
  249. (when-not (and (:create-today-journal? tx-meta)
  250. (:today-journal-name tx-meta)
  251. (seq tx-data)
  252. (d/entity @conn [:block/name (:today-journal-name tx-meta)])) ; today journal created already
  253. ;; (prn :debug :transact :tx-data tx-data :tx-meta tx-meta')
  254. (worker-util/profile "Worker db transact"
  255. (ldb/transact! conn tx-data tx-meta')))
  256. nil)
  257. (catch :default e
  258. (prn :debug :error)
  259. (js/console.error e)))))
  260. (getInitialData
  261. [_this repo]
  262. (when-let [conn (state/get-datascript-conn repo)]
  263. (->> (sqlite-common-db/get-initial-data @conn)
  264. dt/write-transit-str)))
  265. (unsafeUnlinkDB
  266. [_this repo]
  267. (p/let [pool (<get-opfs-pool repo)
  268. _ (close-db! repo)
  269. result (remove-vfs! pool)]
  270. nil))
  271. (releaseAccessHandles
  272. [_this repo]
  273. (when-let [^js pool (state/get-opfs-pool repo)]
  274. (.releaseAccessHandles pool)))
  275. (dbExists
  276. [_this repo]
  277. (<db-exists? repo))
  278. (exportDB
  279. [_this repo]
  280. (<export-db-file repo))
  281. (importDb
  282. [this repo data]
  283. (when-not (string/blank? repo)
  284. (p/let [pool (<get-opfs-pool repo)]
  285. (<import-db pool data))))
  286. ;; Search
  287. (search-blocks
  288. [this repo q option]
  289. (p/let [db (get-search-db repo)
  290. result (search/search-blocks db q (bean/->clj option))]
  291. (bean/->js result)))
  292. (search-upsert-blocks
  293. [this repo blocks]
  294. (p/let [db (get-search-db repo)]
  295. (search/upsert-blocks! db blocks)
  296. nil))
  297. (search-delete-blocks
  298. [this repo ids]
  299. (p/let [db (get-search-db repo)]
  300. (search/delete-blocks! db ids)
  301. nil))
  302. (search-truncate-tables
  303. [this repo]
  304. (p/let [db (get-search-db repo)]
  305. (search/truncate-table! db)
  306. nil))
  307. (search-build-blocks-indice
  308. [this repo]
  309. (when-let [conn (state/get-datascript-conn repo)]
  310. (search/build-blocks-indice repo @conn)))
  311. (search-build-pages-indice
  312. [this repo]
  313. (when-let [conn (state/get-datascript-conn repo)]
  314. (search/build-blocks-indice repo @conn)))
  315. (page-search
  316. [this repo q limit]
  317. (when-let [conn (state/get-datascript-conn repo)]
  318. (search/page-search repo @conn q limit)))
  319. (page-rename
  320. [this repo old-name new-name]
  321. (when-let [conn (state/get-datascript-conn repo)]
  322. (let [config (state/get-config repo)
  323. result (worker-page-rename/rename! repo conn config old-name new-name)]
  324. (bean/->js {:result result}))))
  325. (file-writes-finished?
  326. [this]
  327. (empty? @file/*writes))
  328. (page-file-saved
  329. [this request-id page-id]
  330. (file/dissoc-request! request-id)
  331. nil)
  332. (sync-app-state
  333. [this new-state-str]
  334. (let [new-state (edn/read-string new-state-str)]
  335. (state/set-new-state! new-state)
  336. nil))
  337. ;; RTC
  338. (rtc-start
  339. [this repo token]
  340. (when-let [conn (state/get-datascript-conn repo)]
  341. (rtc-core/<start-rtc repo conn token)
  342. nil))
  343. (rtc-stop
  344. [this]
  345. (rtc-core/<stop-rtc)
  346. nil)
  347. (rtc-toggle-sync
  348. [this repo]
  349. (let [d (p/deferred)]
  350. (async/go
  351. (let [result (<! (rtc-core/<toggle-sync))]
  352. (p/resolve! d result)))
  353. d))
  354. (rtc-grant-graph-access
  355. [this graph-uuid target-user-uuids target-user-emails]
  356. (when-let [state @rtc-core/*state]
  357. (rtc-core/<grant-graph-access-to-others
  358. state graph-uuid
  359. :target-user-uuids target-user-uuids
  360. :target-user-emails target-user-emails))
  361. nil)
  362. (rtc-upload-graph
  363. [this repo token]
  364. (when-let [conn (state/get-datascript-conn repo)]
  365. (async/go
  366. (try
  367. (let [state (<! (rtc-core/<init-state repo token))]
  368. (<! (rtc-updown/<upload-graph state repo conn))
  369. (rtc-db-listener/listen-db-to-generate-ops repo conn))
  370. (worker-util/post-message :notification
  371. (pr-str
  372. [[:div
  373. [:p "Upload graph successfully"]]]))
  374. (catch :default e
  375. (worker-util/post-message :notification
  376. (pr-str
  377. [[:div
  378. [:p "upload graph failed"]]
  379. :error]))
  380. (prn ::download-graph-failed e))))
  381. nil))
  382. (rtc-download-graph
  383. [this repo token graph-uuid]
  384. (async/go
  385. (let [state (<! (rtc-core/<init-state repo token))]
  386. (try
  387. (<? (rtc-updown/<download-graph state repo graph-uuid))
  388. (worker-util/post-message :notification
  389. (pr-str
  390. [[:div
  391. [:p "download graph successfully"]]]))
  392. (catch :default e
  393. (worker-util/post-message :notification
  394. (pr-str
  395. [[:div
  396. [:p "download graph failed"]]
  397. :error]))
  398. (prn ::download-graph-failed e)))))
  399. nil)
  400. (rtc-push-pending-ops
  401. [_this]
  402. (async/put! (:force-push-client-ops-chan @rtc-core/*state) true)
  403. nil)
  404. (rtc-get-graphs
  405. [_this repo token]
  406. (rtc-core/<get-graphs repo token))
  407. (rtc-get-block-content-versions
  408. [_this block-id]
  409. (rtc-core/<get-block-content-versions @rtc-core/*state block-id))
  410. (rtc-get-debug-state
  411. [_this repo]
  412. (bean/->js (rtc-core/get-debug-state repo)))
  413. (dangerousRemoveAllDbs
  414. [this repo]
  415. (p/let [dbs (.listDB this)]
  416. (p/all (map #(.unsafeUnlinkDB this %) dbs)))))
  417. (defn init
  418. "web worker entry"
  419. []
  420. (let [^js obj (DBWorker.)]
  421. (state/set-worker-object! obj)
  422. (file/<ratelimit-file-writes!)
  423. (Comlink/expose obj)))
  424. (comment
  425. (defn <remove-all-files!
  426. "!! Dangerous: use it only for development."
  427. []
  428. (p/let [all-files (<list-all-files)
  429. files (filter #(= (.-kind %) "file") all-files)
  430. dirs (filter #(= (.-kind %) "directory") all-files)
  431. _ (p/all (map (fn [file] (.remove file)) files))]
  432. (p/all (map (fn [dir] (.remove dir)) dirs)))))