mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 23:50:38 +00:00
✨ Keep Penpot tabs connected to MCP from being frozen
Chrome freezes some background tabs, for instance busy ones while Battery Saver is on, and a frozen page cannot run the MCP plugin. Chrome does not freeze a page that holds a Web Lock, so the page holds a lock named penpot-mcp-session from the moment the user asks to connect until the connection stops: on disconnect, logout, MCP being turned off or the MCP runtime starting again. The lock is shared, so several tabs can hold it at once. Browsers without Web Locks skip it, and when the lock request fails, the next connection request tries again. Firefox does not freeze background pages, so the lock changes nothing there.
This commit is contained in:
parent
98a0163d25
commit
9af694e0b6
@ -45,6 +45,32 @@
|
||||
(defonce interval-sub
|
||||
(atom nil))
|
||||
|
||||
;; Resolves the promise that keeps the session Web Lock held; nil when none is.
|
||||
(defonce ^:private session-lock-release
|
||||
(atom nil))
|
||||
|
||||
(defn- hold-session-lock
|
||||
"Holds a shared Web Lock while the MCP connection is requested. Chrome
|
||||
does not freeze a page that holds a Web Lock, and a frozen page cannot
|
||||
run the plugin."
|
||||
[]
|
||||
(when-let [locks (and (nil? @session-lock-release)
|
||||
(unchecked-get globals/navigator "locks"))]
|
||||
(let [released (js/Promise. (fn [resolve] (reset! session-lock-release resolve)))
|
||||
release @session-lock-release]
|
||||
(-> (.request locks "penpot-mcp-session" #js {:mode "shared"} (constantly released))
|
||||
(.catch (fn [cause]
|
||||
;; lets a later connection request try again, unless a
|
||||
;; newer lock request has already replaced this one
|
||||
(compare-and-set! session-lock-release release nil)
|
||||
(log/warn :hint "unable to hold the MCP session lock" :cause cause)))))))
|
||||
|
||||
(defn- release-session-lock
|
||||
[]
|
||||
(when-let [release @session-lock-release]
|
||||
(reset! session-lock-release nil)
|
||||
(release)))
|
||||
|
||||
(defn connect-mcp
|
||||
[]
|
||||
(ptk/reify ::connect-mcp
|
||||
@ -61,7 +87,12 @@
|
||||
(watch [_ state _]
|
||||
(if (get-in state [:mcp :connection-requested])
|
||||
(rx/of (ptk/data-event ::connect))
|
||||
(rx/empty)))))
|
||||
(rx/empty)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (get-in state [:mcp :connection-requested])
|
||||
(hold-session-lock)))))
|
||||
|
||||
(defn- start-reconnect-watcher
|
||||
[]
|
||||
@ -134,6 +165,7 @@
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(release-session-lock)
|
||||
(stop-reconnect-watcher!))))
|
||||
|
||||
(defn- extension-hello?
|
||||
@ -231,6 +263,7 @@
|
||||
(.removeEventListener globals/window "message" on-message)
|
||||
(when @announce?
|
||||
(announce-session "disconnected" nil))
|
||||
(release-session-lock)
|
||||
(stop-reconnect-watcher!)
|
||||
(dp/close-plugin! default-manifest))))
|
||||
(.addEventListener globals/window "message" on-message)
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.workspace.mcp :as mcp]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.globals :as globals]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as a]
|
||||
@ -183,6 +184,47 @@
|
||||
:on-next #(swap! events conj (ptk/type %))))
|
||||
(t/is (= [:app.main.data.workspace.mcp/connect] @events))))
|
||||
|
||||
(defn- connect
|
||||
"Runs the effect of a connection request made with the given MCP state."
|
||||
[mcp-state]
|
||||
(let [event (mcp/connect-mcp)]
|
||||
(ptk/effect event (ptk/update event {:mcp mcp-state}) nil)))
|
||||
|
||||
(t/deftest ^:async test-session-lock-follows-connection-request
|
||||
(let [requests (atom [])
|
||||
released (atom 0)
|
||||
locks #js {:request (fn [lock-name options callback]
|
||||
(swap! requests conj [lock-name (.-mode options)])
|
||||
(.then (callback nil) #(swap! released inc)))}]
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{globals/navigator #js {:locks locks}}
|
||||
(connect {:enabled false})
|
||||
(t/is (empty? @requests))
|
||||
(connect {:enabled true :token-valid true})
|
||||
(connect {:enabled true :token-valid true})
|
||||
(t/is (= [["penpot-mcp-session" "shared"]] @requests))
|
||||
(ptk/effect (mcp/user-disconnect-mcp) nil nil)
|
||||
(await (a/settle))
|
||||
(t/is (= 1 @released))
|
||||
(ptk/effect (mcp/user-disconnect-mcp) nil nil)
|
||||
(await (a/settle))
|
||||
(t/is (= 1 @released))))))
|
||||
|
||||
(t/deftest ^:async test-session-lock-retried-after-failed-request
|
||||
(let [requests (atom 0)
|
||||
locks #js {:request (fn [_ _ _]
|
||||
(swap! requests inc)
|
||||
(js/Promise.reject (js/Error. "lock request denied")))}]
|
||||
(await
|
||||
(mock/with-mocks*
|
||||
{globals/navigator #js {:locks locks}}
|
||||
(connect {:enabled true :token-valid true})
|
||||
(await (a/settle))
|
||||
(connect {:enabled true :token-valid true})
|
||||
(await (a/settle))
|
||||
(t/is (= 2 @requests))))))
|
||||
|
||||
(t/deftest test-disconnect-clears-connection-intent
|
||||
(let [state {:mcp {:connection-requested true
|
||||
:connection-status "connecting"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user