Files
logseq/src/main/frontend/persist_db/browser.cljs
2024-01-09 14:09:12 +08:00

162 lines
6.2 KiB
Clojure

(ns frontend.persist-db.browser
"Browser db persist support, using @logseq/sqlite-wasm.
This interface uses clj data format as input."
(:require ["comlink" :as Comlink]
[frontend.persist-db.protocol :as protocol]
[frontend.config :as config]
[promesa.core :as p]
[frontend.util :as util]
[frontend.handler.notification :as notification]
[cljs-bean.core :as bean]
[frontend.state :as state]
[electron.ipc :as ipc]
[frontend.handler.worker :as worker-handler]
[logseq.db :as ldb]))
(defonce *worker (atom nil))
(defn- ask-persist-permission!
[]
(p/let [persistent? (.persist js/navigator.storage)]
(if persistent?
(js/console.log "Storage will not be cleared unless from explicit user action")
(js/console.warn "OPFS storage may be cleared by the browser under storage pressure."))))
(defn- sync-app-state!
[^js worker]
(add-watch state/state
:sync-worker-state
(fn [_ _ prev current]
(let [new-state (cond-> {}
(not= (:git/current-repo prev)
(:git/current-repo current))
(assoc :git/current-repo (:git/current-repo current))
(not= (:config prev) (:config current))
(assoc :config (:config current)))]
(when (seq new-state)
(.sync-app-state worker (pr-str new-state)))))))
(defn- transact!
[^js worker repo tx-data tx-meta]
(let [tx-meta' (pr-str tx-meta)
tx-data' (pr-str tx-data)
context {:dev? config/dev?
:node-test? util/node-test?
:validate-db-options (:dev/validate-db-options (state/get-config))
:importing? (:graph/importing @state/state)
:date-formatter (state/get-date-formatter)
:export-bullet-indentation (state/get-export-bullet-indentation)
:preferred-format (state/get-preferred-format)
:journals-directory (config/get-journals-directory)
:whiteboards-directory (config/get-whiteboards-directory)
:pages-directory (config/get-pages-directory)}]
(if worker
(.transact worker repo tx-data' tx-meta'
(pr-str context))
(notification/show! "Latest change was not saved! Please restart the application." :error))))
(defn start-db-worker!
[]
(when-not (or config/publishing? util/node-test?)
(let [worker-url (if (util/electron?)
"js/db-worker.js"
"/static/js/db-worker.js")
worker (js/Worker. (str worker-url "?electron=" (util/electron?)))
wrapped-worker (Comlink/wrap worker)]
(worker-handler/handle-message! worker)
(reset! *worker wrapped-worker)
(-> (p/let [_ (.init wrapped-worker config/RTC-WS-URL)
_ (.sync-app-state wrapped-worker
(pr-str
{:git/current-repo (state/get-current-repo)
:config (:config @state/state)}))
_ (sync-app-state! wrapped-worker)
_ (ask-persist-permission!)]
(ldb/register-transact-fn!
(fn worker-transact!
[_conn tx-data tx-meta]
(transact! wrapped-worker (state/get-current-repo) tx-data
;; not from remote(rtc)
(assoc tx-meta :local-tx? true)))))
(p/catch (fn [error]
(prn :debug "Can't init SQLite wasm")
(js/console.error error)
(notification/show! "It seems that OPFS is not supported on this browser, please upgrade it to the latest version or use another browser." :error)))))))
(defn <export-db!
[repo data]
(cond
(util/electron?)
(ipc/ipc :db-export repo data)
;; TODO: browser nfs-supported? auto backup
;;
:else
nil))
(defn- sqlite-error-handler
[error]
(if (= "NoModificationAllowedError" (.-name error))
(state/pub-event! [:show/multiple-tabs-error-dialog])
(notification/show! [:div (str "SQLiteDB error: " error)] :error)))
(defrecord InBrowser []
protocol/PersistentDB
(<new [_this repo]
(when-let [^js sqlite @*worker]
(.createOrOpenDB sqlite repo)))
(<list-db [_this]
(when-let [^js sqlite @*worker]
(-> (.listDB sqlite)
(p/then (fn [result]
(bean/->clj result)))
(p/catch sqlite-error-handler))))
(<unsafe-delete [_this repo]
(when-let [^js sqlite @*worker]
(.unsafeUnlinkDB sqlite repo)))
(<release-access-handles [_this repo]
(when-let [^js sqlite @*worker]
(.releaseAccessHandles sqlite repo)))
(<fetch-initial-data [_this repo _opts]
(when-let [^js sqlite @*worker]
(-> (p/let [db-exists? (.dbExists sqlite repo)
disk-db-data (when-not db-exists? (ipc/ipc :db-get repo))
_ (when disk-db-data
(.importDb sqlite repo disk-db-data))
_ (.createOrOpenDB sqlite repo)]
(.getInitialData sqlite repo))
(p/catch sqlite-error-handler))))
(<export-db [_this repo opts]
(when-let [^js sqlite @*worker]
(-> (p/let [data (.exportDB sqlite repo)]
(when data
(if (:return-data? opts)
data
(<export-db! repo data))))
(p/catch (fn [error]
(prn :debug :save-db-error repo)
(js/console.error error)
(notification/show! [:div (str "SQLiteDB save error: " error)] :error) {})))))
(<import-db [_this repo data]
(when-let [^js sqlite @*worker]
(-> (.importDb sqlite repo data)
(p/catch (fn [error]
(prn :debug :import-db-error repo)
(js/console.error error)
(notification/show! [:div (str "SQLiteDB import error: " error)] :error) {}))))))
(comment
(defn clean-all-dbs!
[]
(when-let [sqlite @*sqlite]
(.dangerousRemoveAllDbs sqlite)
(state/set-current-repo! nil))))