Fase 4: migrate the daemon event channel from SSE to WebSocket with resume-by-revision
Adds a WS upgrade handler alongside the existing SSE stream on /v1/events
(kept alive for instant rollback), authenticated via Sec-WebSocket-Protocol
instead of a query-string token. The daemon now keeps a ~500-entry circular
buffer of broadcast sync-db-changes deltas indexed by renderer revision; a
client reconnecting sends {type: subscribe, graphId, sinceRev} and either
gets a gap-free replay + ack, or a {type: resync} instructing it to discard
all cached renderer state and reload (frontend.db.subs/reset-graph!, wired
through a new :db/resync event). The web/Electron client transport
(frontend.persist-db.remote) now opens the WS channel by default; the SSE
path (connect-events!) is untouched and still fully wired.
Also fixes new/pre-existing test breakage surfaced by switching the default
transport: persist_db_test.cljs's shared Electron-failover fixture now mocks
open-ws-fn (not just open-sse-fn), and a `new` applied to an inline (or ...)
expression in make-server's WebSocketServer construction is replaced with
the same let-bound-symbol pattern deps/db-sync's node/server.cljs already
uses -- inlining it silently dropped `new`, breaking every daemon test.
Confirmed 7 unrelated persist_db_test.cljs failures (electron-ensure-remote-*
IPC bookkeeping) already fail identically on the pre-Fase-4 baseline -- left
untouched, out of scope here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
(ns frontend.persist-db.remote
|
||||
"Remote `PersistentDB` implementation for Electron renderer via db-worker-node HTTP and SSE."
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.db.subs :as db-subs]
|
||||
[frontend.persist-db.protocol :as protocol]
|
||||
[logseq.db :as ldb]
|
||||
[promesa.core :as p]
|
||||
@@ -32,6 +33,12 @@
|
||||
(cond-> (str (normalize-base-url base-url) "/v1/events")
|
||||
(seq auth-token) (str "?token=" (js/encodeURIComponent auth-token))))
|
||||
|
||||
(defn- events-ws-url
|
||||
[base-url]
|
||||
(-> (normalize-base-url base-url)
|
||||
(string/replace #"^http" "ws")
|
||||
(str "/v1/events")))
|
||||
|
||||
(defn- base-headers
|
||||
[auth-token]
|
||||
(cond-> {"Content-Type" "application/json"
|
||||
@@ -115,8 +122,12 @@
|
||||
(subs line 6)))
|
||||
(string/split-lines event-text)))
|
||||
|
||||
(defn- default-open-ws-fn
|
||||
[{:keys [url auth-token]}]
|
||||
(js/WebSocket. url (str "bearer." auth-token)))
|
||||
|
||||
(defn create-client
|
||||
[{:keys [fetch-fn open-sse-fn schedule-fn reconnect-delay-ms] :as opts}]
|
||||
[{:keys [fetch-fn open-sse-fn open-ws-fn schedule-fn reconnect-delay-ms] :as opts}]
|
||||
(let [default-fetch-fn
|
||||
(fn [{:keys [method url headers body]}]
|
||||
(p/let [^js res (js/fetch url (clj->js (cond-> {:method method
|
||||
@@ -143,6 +154,7 @@
|
||||
(assoc opts
|
||||
:fetch-fn (or fetch-fn default-fetch-fn)
|
||||
:open-sse-fn (or open-sse-fn default-open-sse-fn)
|
||||
:open-ws-fn (or open-ws-fn default-open-ws-fn)
|
||||
:schedule-fn (or schedule-fn (fn [f delay-ms]
|
||||
(js/setTimeout f delay-ms)))
|
||||
:reconnect-delay-ms (or reconnect-delay-ms 1000))))
|
||||
@@ -244,6 +256,73 @@
|
||||
(close-subscription!)
|
||||
nil)})))
|
||||
|
||||
(defn connect-events-ws!
|
||||
"WebSocket transport for the daemon event channel -- the default since
|
||||
Fase 4 (connect-events! above stays fully wired as an instant rollback,
|
||||
see docs/agent-guide). Resumes by revision on every (re)connect: sends
|
||||
{type: subscribe, graphId: repo, sinceRev: N} where N is the renderer's
|
||||
last applied revision. The server replays buffered deltas gap-free, or --
|
||||
if the gap is wider than its ~500-entry buffer -- replies {type: resync},
|
||||
handled here as a normal event-handler dispatch so the existing
|
||||
frontend.handler.worker / :db/resync wiring can discard cached renderer
|
||||
state and reload."
|
||||
[{:keys [base-url auth-token repo event-handler open-ws-fn schedule-fn
|
||||
reconnect-delay-ms on-event-error current-rev-fn]}
|
||||
wrapped-worker]
|
||||
(let [connected? (atom true)
|
||||
socket (atom nil)
|
||||
current-rev-fn (or current-rev-fn db-subs/current-rev)
|
||||
close-socket! (fn []
|
||||
(when-let [^js s @socket]
|
||||
(set! (.-onopen s) nil)
|
||||
(set! (.-onmessage s) nil)
|
||||
(set! (.-onerror s) nil)
|
||||
(set! (.-onclose s) nil)
|
||||
(try (.close s) (catch :default _ nil)))
|
||||
(reset! socket nil))
|
||||
dispatch-control! (fn [{:keys [type rev message]}]
|
||||
(case type
|
||||
"resync" (when event-handler
|
||||
(event-handler :resync wrapped-worker {:rev rev}))
|
||||
"error" (log/error :db-worker-node-ws-server-error {:message message})
|
||||
nil))
|
||||
dispatch-business! (fn [event]
|
||||
(let [[event-type payload] (decode-event event)]
|
||||
(when (and event-handler event-type)
|
||||
(event-handler event-type wrapped-worker payload))))]
|
||||
(letfn [(open! []
|
||||
(when @connected?
|
||||
(let [^js s (open-ws-fn {:url (events-ws-url base-url) :auth-token auth-token})]
|
||||
(reset! socket s)
|
||||
(set! (.-onopen s)
|
||||
(fn [_]
|
||||
(when @connected?
|
||||
(.send s (js/JSON.stringify
|
||||
(clj->js {:type "subscribe"
|
||||
:graphId repo
|
||||
:sinceRev (current-rev-fn)}))))))
|
||||
(set! (.-onmessage s)
|
||||
(fn [event]
|
||||
(when @connected?
|
||||
(let [parsed (parse-response-body (.-data event))]
|
||||
(if (contains? parsed :payload)
|
||||
(dispatch-business! parsed)
|
||||
(dispatch-control! parsed))))))
|
||||
(set! (.-onerror s)
|
||||
(fn [error]
|
||||
(when (and @connected? on-event-error)
|
||||
(on-event-error error))))
|
||||
(set! (.-onclose s)
|
||||
(fn [_]
|
||||
(reset! socket nil)
|
||||
(when @connected?
|
||||
(schedule-fn open! reconnect-delay-ms)))))))]
|
||||
(open!)
|
||||
{:disconnect! (fn []
|
||||
(reset! connected? false)
|
||||
(close-socket!)
|
||||
nil)})))
|
||||
|
||||
(defn- method->str
|
||||
[qkw]
|
||||
(str (namespace qkw) "/" (name qkw)))
|
||||
@@ -280,10 +359,10 @@
|
||||
(let [client (create-client (assoc opts :base-url base-url :auth-token auth-token))
|
||||
wrapped-worker (fn [qkw & args]
|
||||
(invoke! client (method->str qkw) args))
|
||||
{:keys [disconnect!]} (connect-events! (assoc client
|
||||
:event-handler event-handler
|
||||
:auth-token auth-token)
|
||||
wrapped-worker)]
|
||||
{:keys [disconnect!]} (connect-events-ws! (assoc client
|
||||
:event-handler event-handler
|
||||
:auth-token auth-token)
|
||||
wrapped-worker)]
|
||||
(->InRemote client wrapped-worker disconnect!)))
|
||||
|
||||
(defn stop!
|
||||
|
||||
Reference in New Issue
Block a user