db_worker.cljs 17 KB

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