Fase 3: make the remote transport config-driven, not Electron-only
Every PersistentDB dispatch site in persist_db.cljs branched on electron-runtime?, so a web or mobile client could never take the remote path -- only Electron's IPC-managed local daemon could. The remote client itself (persist_db/remote.cljs) was already platform-agnostic (plain js/fetch and js/EventSource), so this is a transport-selection fix, not a new architecture. - frontend.persist-db/remote-runtime?: true for Electron OR a non-Electron client with config/get-server-base-url configured. Replaces every electron-runtime? check at the seven PersistentDB call sites (<start-runtime!, <list-db, <unsafe-delete, <close-db, <export-db, <import-db, <open-and-fetch-schema, <new). - Fixes the known <export-db bug: it always went through the local OPFS impl regardless of transport, so exporting a remote graph silently exported nothing. It now follows the same remote-runtime? dispatch as every sibling function. - <resolve-remote-runtime!/<release-remote-runtime-slot! split the Electron-IPC path from a new web path that just reads the locally configured base-url/token -- there's no daemon to spawn from a browser, only one to log into. - config.cljs: get/set-server-base-url!, get/set-server-token!, clear-server-config!, mirroring the existing custom-sync-server-url localStorage pattern. - remote.cljs: login! POSTs to the daemon's /v1/auth/login (added in Fase 1) and returns a bearer token. - A minimal settings panel (components/settings.cljs, non-Electron only): server URL + username + password, calls login!, saves the result, and reloads. Functional but not visually verified -- no browser available in this environment; the request/response wiring is confirmed below against a real daemon, not the rendered UI itself. Verified by running a real db-worker-node daemon and exercising the exact sequence the new settings panel and remote.cljs drive: unauthenticated invoke -> 401, login with correct/wrong credentials -> token/401, healthz unauthenticated with :pid/:root-dir absent, authenticated invoke (list-db) -> 200, per-origin CORS reflection, bad Host -> 403, and SSE both without a token (401) and with one as a query param (200). Plus clojure -M:cljs compile app db-worker-node electron (0 warnings) and the full logseq.db-worker.daemon-test, logseq.cli.server-test, frontend.worker.db-worker-node-test, electron.db-worker-manager-test, frontend.state-test, frontend.persist-db.remote-test suites (134 tests, 541 assertions, 0 failures). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.modules.instrumentation.core :as instrument]
|
||||
[frontend.modules.shortcut.data-helper :as shortcut-helper]
|
||||
[frontend.persist-db.remote :as remote]
|
||||
[frontend.rfx :as rfx]
|
||||
[frontend.spec.storage :as storage-spec]
|
||||
[frontend.state :as state]
|
||||
@@ -689,6 +690,104 @@
|
||||
:on-click (fn [] (reset-url!))}
|
||||
(t :settings.sync-server/reset)))]]]))
|
||||
|
||||
(hsx/defc db-worker-server-settings-container
|
||||
"Connects a web/mobile client to a self-hosted db-worker-node server (see
|
||||
frontend.persist-db/remote-runtime?). Electron manages its own local
|
||||
daemon instead and doesn't need this. Reconnecting or disconnecting
|
||||
requires a reload since frontend.persist-db/<start-runtime! only decides
|
||||
the transport once at boot."
|
||||
[]
|
||||
(let [current-url (config/get-server-base-url)
|
||||
connected? (and (seq current-url) (seq (config/get-server-token)))
|
||||
[url set-url!] (hooks/use-state (or current-url ""))
|
||||
[username set-username!] (hooks/use-state "")
|
||||
[password set-password!] (hooks/use-state "")
|
||||
[connecting? set-connecting!] (hooks/use-state false)
|
||||
disconnect! (fn []
|
||||
(config/clear-server-config!)
|
||||
(notification/show! "Disconnected. Reloading..." :success)
|
||||
(js/setTimeout #(js/location.reload) 500))
|
||||
connect! (fn []
|
||||
(let [trimmed-url (string/trim url)]
|
||||
(cond
|
||||
(string/blank? trimmed-url)
|
||||
(notification/show! "Server URL is required." :error)
|
||||
|
||||
(not (config/valid-sync-server-url? trimmed-url))
|
||||
(notification/show! "Enter a valid http(s) URL." :error)
|
||||
|
||||
(or (string/blank? username) (string/blank? password))
|
||||
(notification/show! "Username and password are required." :error)
|
||||
|
||||
:else
|
||||
(do
|
||||
(set-connecting! true)
|
||||
(-> (remote/login! {:base-url trimmed-url
|
||||
:username username
|
||||
:password password
|
||||
:device-label (str (.-platform js/navigator))})
|
||||
(p/then (fn [{:keys [token]}]
|
||||
(config/set-server-base-url! trimmed-url)
|
||||
(config/set-server-token! token)
|
||||
(notification/show! "Connected. Reloading..." :success)
|
||||
(js/setTimeout #(js/location.reload) 500)))
|
||||
(p/catch (fn [error]
|
||||
(set-connecting! false)
|
||||
(notification/show!
|
||||
(str "Failed to connect: " (or (.-message error) error))
|
||||
:error))))))))]
|
||||
[:div.cp__settings-db-worker-server-cnt
|
||||
[:h1.mb-2.text-2xl.font-bold "Server"]
|
||||
[:div.p-2
|
||||
[:p.text-sm.opacity-70.mb-4
|
||||
"Connect this client to a self-hosted Logseq server (db-worker-node) instead of storing graphs locally in the browser."]
|
||||
(if connected?
|
||||
[:div
|
||||
[:p [:strong "Connected to: "] current-url]
|
||||
[:p.pt-2
|
||||
(shui/button
|
||||
{:size :sm :variant :outline :on-click disconnect!}
|
||||
"Disconnect")]]
|
||||
[:div
|
||||
[:p
|
||||
[:label [:strong "Server URL"]
|
||||
[:input.form-input.is-small
|
||||
{:value url
|
||||
:placeholder "https://your-server.example:PORT"
|
||||
:style {:width "100%"}
|
||||
:on-change #(set-url! (util/evalue %))}]]]
|
||||
[:p.pt-2
|
||||
[:label [:strong "Username"]
|
||||
[:input.form-input.is-small
|
||||
{:value username
|
||||
:style {:width "100%"}
|
||||
:on-change #(set-username! (util/evalue %))}]]]
|
||||
[:p.pt-2
|
||||
[:label [:strong "Password"]
|
||||
[:input.form-input.is-small
|
||||
{:type "password"
|
||||
:value password
|
||||
:style {:width "100%"}
|
||||
:on-change #(set-password! (util/evalue %))}]]]
|
||||
[:p.pt-2
|
||||
(shui/button
|
||||
{:size :sm :disabled connecting? :on-click connect!}
|
||||
(if connecting? "Connecting..." "Connect"))]])]]))
|
||||
|
||||
(hsx/defc db-worker-server-button
|
||||
[]
|
||||
(let [current-url (config/get-server-base-url)]
|
||||
(ui/button [:span.flex.items-center
|
||||
[:span.pr-1 (if (seq current-url) current-url "Connect to server")]
|
||||
(ui/icon "edit")]
|
||||
:class "text-sm"
|
||||
:on-click #(state/pub-event! [:go/db-worker-server-settings]))))
|
||||
|
||||
(defn db-worker-server-row []
|
||||
(row-with-button-action
|
||||
{:left-label "Server"
|
||||
:action (db-worker-server-button)}))
|
||||
|
||||
(hsx/defc sync-server-url-button
|
||||
[]
|
||||
(let [current-url (config/get-custom-sync-server-url)]
|
||||
@@ -924,6 +1023,7 @@
|
||||
(when (and (or util/mac? util/win32?) (util/electron?)) (app-auto-update-row t))
|
||||
(usage-diagnostics-row t instrument-disabled?)
|
||||
(when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
|
||||
(when-not (util/electron?) (db-worker-server-row))
|
||||
(sync-server-url-row)
|
||||
(publish-server-url-row)
|
||||
(when (util/electron?) (https-user-agent-row https-agent-opts))
|
||||
|
||||
@@ -99,6 +99,53 @@
|
||||
(custom-url->http-base custom)
|
||||
default-db-sync-http-base))
|
||||
|
||||
;; Web/mobile clients connect to a self-hosted db-worker-node daemon by
|
||||
;; base URL + bearer token, instead of Electron's IPC-managed local runtime
|
||||
;; (see frontend.persist-db/remote-runtime?). Stored client-side only; the
|
||||
;; daemon holds the authoritative credential (see
|
||||
;; frontend.worker.db-worker-node-auth).
|
||||
(defn get-server-base-url
|
||||
"Read the configured db-worker-node server base URL from localStorage.
|
||||
Returns nil when not set or empty."
|
||||
[]
|
||||
(when-not util/node-test?
|
||||
(let [v (.getItem js/localStorage "db-worker-server-url")]
|
||||
(when (and (string? v) (not (string/blank? v)))
|
||||
v))))
|
||||
|
||||
(defn set-server-base-url!
|
||||
"Persist the db-worker-node server base URL to localStorage. Pass nil or
|
||||
empty string to clear."
|
||||
[url]
|
||||
(when-not util/node-test?
|
||||
(if (or (nil? url) (string/blank? url))
|
||||
(.removeItem js/localStorage "db-worker-server-url")
|
||||
(.setItem js/localStorage "db-worker-server-url" (string/trim url)))))
|
||||
|
||||
(defn get-server-token
|
||||
"Read the current db-worker-node session bearer token from localStorage.
|
||||
Returns nil when not set or empty."
|
||||
[]
|
||||
(when-not util/node-test?
|
||||
(let [v (.getItem js/localStorage "db-worker-server-token")]
|
||||
(when (and (string? v) (not (string/blank? v)))
|
||||
v))))
|
||||
|
||||
(defn set-server-token!
|
||||
"Persist the db-worker-node session bearer token to localStorage. Pass
|
||||
nil or empty string to clear (e.g. on logout or a 401)."
|
||||
[token]
|
||||
(when-not util/node-test?
|
||||
(if (or (nil? token) (string/blank? token))
|
||||
(.removeItem js/localStorage "db-worker-server-token")
|
||||
(.setItem js/localStorage "db-worker-server-token" (string/trim token)))))
|
||||
|
||||
(defn clear-server-config!
|
||||
"Clear both the configured server URL and session token, e.g. on logout."
|
||||
[]
|
||||
(set-server-base-url! nil)
|
||||
(set-server-token! nil))
|
||||
|
||||
(defn get-custom-publish-server-url
|
||||
"Read the user-configured custom publish server URL from localStorage.
|
||||
Returns nil when not set or empty."
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
(settings/sync-server-url-settings-container)
|
||||
{:id :sync-server-panel :center? true :class "lg:max-w-2xl"}))
|
||||
|
||||
(defevent! :go/db-worker-server-settings [[_]]
|
||||
(shui/dialog-open!
|
||||
(settings/db-worker-server-settings-container)
|
||||
{:id :db-worker-server-panel :center? true :class "lg:max-w-2xl"}))
|
||||
|
||||
(defevent! :go/publish-server-settings [[_]]
|
||||
(shui/dialog-open!
|
||||
(settings/publish-server-url-settings-container)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
(defonce remote-repo (atom nil))
|
||||
(defonce remote-runtime-state (atom nil))
|
||||
|
||||
(declare <ensure-remote!)
|
||||
(declare <ensure-remote! <release-remote-runtime-slot!)
|
||||
|
||||
(defn- clear-remote-runtime!
|
||||
[]
|
||||
@@ -105,7 +105,7 @@
|
||||
(if (active-runtime-client? @remote-runtime-state repo session-id remote-client)
|
||||
(p/let [_ (do
|
||||
(clear-remote-runtime!)
|
||||
(ipc/ipc "releaseDbWorkerRuntime" repo))]
|
||||
(<release-remote-runtime-slot! repo))]
|
||||
true)
|
||||
(do
|
||||
(log/info :event :db-worker-runtime-recovery-skipped
|
||||
@@ -178,6 +178,48 @@
|
||||
(and (not (node-runtime?))
|
||||
(util/electron?)))
|
||||
|
||||
(defn- web-remote-configured?
|
||||
"True when running as a non-Electron, non-Node client (browser or
|
||||
Capacitor) with a db-worker-node server configured -- the counterpart to
|
||||
Electron's IPC-managed local runtime, driven by config/get-server-base-url
|
||||
instead."
|
||||
[]
|
||||
(and (not (node-runtime?))
|
||||
(not (util/electron?))
|
||||
(some? (config/get-server-base-url))))
|
||||
|
||||
(defn- remote-runtime?
|
||||
"True whenever this client talks to a remote db-worker-node daemon over
|
||||
HTTP/SSE rather than an in-process worker (frontend.persist-db.browser).
|
||||
Replaces the old electron-runtime? checks at every PersistentDB dispatch
|
||||
site below, so the same remote path serves Electron and a configured web
|
||||
or mobile client alike."
|
||||
[]
|
||||
(or (electron-runtime?)
|
||||
(web-remote-configured?)))
|
||||
|
||||
(defn- <resolve-remote-runtime!
|
||||
"Resolves {:base-url :auth-token} for the active remote transport.
|
||||
Electron gets it from the main process, which owns spawning/reusing the
|
||||
local daemon (see logseq.cli.server/ensure-server!); a web/mobile client
|
||||
reads its own locally configured server instead -- there is no daemon to
|
||||
spawn, only one to log into."
|
||||
[repo]
|
||||
(if (electron-runtime?)
|
||||
(ipc/ipc "db-worker-runtime" repo)
|
||||
(p/resolved {:base-url (config/get-server-base-url)
|
||||
:auth-token (config/get-server-token)})))
|
||||
|
||||
(defn- <release-remote-runtime-slot!
|
||||
"Releases the Electron-owned runtime slot for repo, if any. No-op for a
|
||||
web/mobile client: there's no per-window runtime manager on the other
|
||||
end to release, just the daemon's own session, which the client simply
|
||||
stops using."
|
||||
[repo]
|
||||
(if (electron-runtime?)
|
||||
(ipc/ipc "releaseDbWorkerRuntime" repo)
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn- current-db-sync-config
|
||||
[]
|
||||
{:enabled? true
|
||||
@@ -243,7 +285,7 @@
|
||||
(log/warn :event :db-worker-ensure-remote-stale
|
||||
:repo repo :phase :before-runtime)
|
||||
nil)
|
||||
(p/let [runtime (ipc/ipc "db-worker-runtime" repo)
|
||||
(p/let [runtime (<resolve-remote-runtime! repo)
|
||||
client (remote/start! (assoc runtime
|
||||
:repo repo
|
||||
:event-handler worker-handler/handle
|
||||
@@ -278,7 +320,7 @@
|
||||
(log/info :event :db-worker-stale-release-skipped
|
||||
:repo repo
|
||||
:reason :runtime-changed)
|
||||
(ipc/ipc "releaseDbWorkerRuntime" repo))]
|
||||
(<release-remote-runtime-slot! repo))]
|
||||
nil)))
|
||||
(p/catch (fn [e]
|
||||
(log/warn :event :db-worker-stale-release-error
|
||||
@@ -302,7 +344,7 @@
|
||||
(defn <start-runtime!
|
||||
[]
|
||||
(cond
|
||||
(electron-runtime?)
|
||||
(remote-runtime?)
|
||||
(if-let [repo (state/get-current-repo)]
|
||||
(<ensure-remote! repo)
|
||||
(p/resolved nil))
|
||||
@@ -316,7 +358,7 @@
|
||||
opfs-db)
|
||||
|
||||
(defn <list-db []
|
||||
(if (electron-runtime?)
|
||||
(if (remote-runtime?)
|
||||
(if-let [repo (or @remote-repo (state/get-current-repo))]
|
||||
(p/let [client (<ensure-remote! repo)]
|
||||
(protocol/<list-db client))
|
||||
@@ -325,14 +367,14 @@
|
||||
|
||||
(defn <unsafe-delete [repo]
|
||||
(when repo
|
||||
(if (electron-runtime?)
|
||||
(if (remote-runtime?)
|
||||
(p/let [client (<ensure-remote! repo)]
|
||||
(protocol/<unsafe-delete client repo))
|
||||
(protocol/<unsafe-delete (get-impl) repo))))
|
||||
|
||||
(defn <close-db [repo]
|
||||
(when repo
|
||||
(if (electron-runtime?)
|
||||
(if (remote-runtime?)
|
||||
(if (same-remote-repo? repo @remote-repo)
|
||||
(if-let [remote-client @remote-db]
|
||||
(p/let [_ (-> (remote/invoke! (:client remote-client) "thread-api/close-db" [repo])
|
||||
@@ -346,12 +388,15 @@
|
||||
(defn <export-db
|
||||
[repo opts]
|
||||
(when repo
|
||||
(protocol/<export-db (get-impl) repo opts)))
|
||||
(if (remote-runtime?)
|
||||
(p/let [client (<ensure-remote! repo)]
|
||||
(protocol/<export-db client repo opts))
|
||||
(protocol/<export-db (get-impl) repo opts))))
|
||||
|
||||
(defn <import-db
|
||||
[repo data]
|
||||
(when repo
|
||||
(if (electron-runtime?)
|
||||
(if (remote-runtime?)
|
||||
(p/let [client (<ensure-remote! repo)]
|
||||
(protocol/<import-db client repo data))
|
||||
(protocol/<import-db (get-impl) repo data))))
|
||||
@@ -361,7 +406,7 @@
|
||||
(<open-and-fetch-schema repo {}))
|
||||
([repo opts]
|
||||
(when repo
|
||||
(if (electron-runtime?)
|
||||
(if (remote-runtime?)
|
||||
(p/let [client (<ensure-remote! repo)]
|
||||
(protocol/<open-and-fetch-schema client repo opts))
|
||||
(protocol/<open-and-fetch-schema (get-impl) repo opts)))))
|
||||
@@ -370,7 +415,7 @@
|
||||
;; @shuyu Do we still need this?
|
||||
(defn <new [repo opts]
|
||||
{:pre [(<= (count repo) 128)]}
|
||||
(p/let [impl (if (electron-runtime?)
|
||||
(p/let [impl (if (remote-runtime?)
|
||||
(<ensure-remote! repo)
|
||||
(p/resolved (get-impl)))
|
||||
_ (protocol/<new impl repo opts)]
|
||||
|
||||
@@ -57,6 +57,44 @@
|
||||
(string? code) (keyword code)
|
||||
:else :invoke-failed))
|
||||
|
||||
(defn- login-url
|
||||
[base-url]
|
||||
(str (normalize-base-url base-url) "/v1/auth/login"))
|
||||
|
||||
(defn- default-fetch-json!
|
||||
[{:keys [method url headers body]}]
|
||||
(p/let [^js res (js/fetch url (clj->js (cond-> {:method method
|
||||
:headers (or headers {})}
|
||||
body (assoc :body body))))
|
||||
text (.text res)]
|
||||
{:status (.-status res)
|
||||
:body text}))
|
||||
|
||||
(defn login!
|
||||
"Logs into a db-worker-node daemon's HTTP auth (see
|
||||
frontend.worker.db-worker-node-auth) and returns {:token :session-id} on
|
||||
success. Used by the web/mobile server-connection settings UI; Electron
|
||||
gets its token from the main process instead (see
|
||||
frontend.persist-db/<resolve-remote-runtime!)."
|
||||
[{:keys [base-url username password device-label fetch-fn]}]
|
||||
(p/let [payload (js/JSON.stringify
|
||||
(clj->js {:username username
|
||||
:password password
|
||||
:deviceLabel device-label}))
|
||||
{:keys [status body]} ((or fetch-fn default-fetch-json!)
|
||||
{:method "POST"
|
||||
:url (login-url base-url)
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body payload})
|
||||
parsed (parse-response-body body)]
|
||||
(if (<= 200 status 299)
|
||||
{:token (:token parsed) :session-id (:sessionId parsed)}
|
||||
(let [error (:error parsed)]
|
||||
(throw (ex-info (or (:message error) "login failed")
|
||||
(cond-> {:status status
|
||||
:code (normalize-code (:code error))}
|
||||
error (assoc :error error))))))))
|
||||
|
||||
(defn- decode-event
|
||||
[{:keys [type payload]}]
|
||||
(let [decoded (when (some? payload)
|
||||
|
||||
Reference in New Issue
Block a user