fix(browser): keep the extension bridge connection alive and its status in sync#113
Conversation
…us in sync
The jcode Browser Bridge extension flapped between connected and
"Reconnecting…" every 1–3 minutes, and could get stranded showing
"Reconnecting…" (with "Could not reach the jcode server") even while the
socket was up and browser tools worked.
Two root causes:
1. No server-side keepalive. After the hello handshake the bridge cleared
the read deadline and never sent anything, so the only thing holding the
extension's MV3 service worker awake was its own 30s chrome.alarms ping —
which races the 30s idle-kill (and is clamped to 60s on some builds). When
the worker napped the socket dropped and only came back on the next alarm
tick. Fix: the server now sends an application-level {"type":"ping"} every
15s (an inbound ws message resets the worker's idle timer) and drops the
peer if no frame arrives within 40s.
2. Stale-socket state clobber. All ws event handlers referenced the module-
level `ws`, so a superseded socket's late-firing onclose set connected=false
after a newer socket had already welcomed; the keepalive alarm's reconnect
then early-returned because the (live) global ws was OPEN, leaving connected
stuck false forever. Fix: capture each socket in a local `sock` and guard
every handler with `ws === sock`; also self-heal `connected` when a server
ping arrives (receiving one proves we're connected).
Also: quiet the popup so a routine reconnect shows a neutral "Reconnecting to
jcode…" instead of the red "Could not reach the jcode server" error, which now
only appears after several failed attempts in a row.
Adds TestBridgeKeepAlivePing covering the server ping + read watchdog.
Generated with Jack AI bot
📝 WalkthroughWalkthroughThe extension background script now guards against stale WebSocket instances and handles ping/pong keep-alive frames, refining reconnect/error UI logic in the popup. The Go bridge adds a server-side keep-alive goroutine, centralizes writes through a mutex-protected writeJSON, extends read deadlines, and recognizes ping/pong frames, with an accompanying test. ChangesExtension connection stability
Server-side bridge keep-alive
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Extension as Background.js
participant Bridge as Go Bridge
participant Popup
Extension->>Bridge: WebSocket connect (sock)
Bridge->>Extension: welcome + periodic ping
Extension->>Bridge: pong response
Bridge->>Bridge: readLoop extends read deadline
Note over Extension,Bridge: If ws !== sock, stale events ignored
Bridge-->>Extension: connection closed (no ping response)
Extension->>Popup: update status (info or error)
Popup->>Popup: render "Reconnecting..." or error message
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/browser/bridge.go (1)
203-217: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winClose the websocket when
readLoopexits
readLoopreturns on read timeout without closingc.ws, so a silent-but-connected client can leave the underlying socket/fd open until something else tears it down. Adddefer c.close()(or close it on the error path) so the keep-alive timeout actually releases the connection.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/browser/bridge.go` around lines 203 - 217, The readLoop exit path leaves the websocket open on read timeout, so the underlying connection can remain allocated. Update bridgeConn.readLoop to ensure c.close is invoked when the loop exits, either by adding a deferred close at the start of readLoop or by closing c.ws in the ReadJSON error path alongside the existing pending-channel cleanup. Keep the fix localized to bridgeConn and preserve the current closeErr/closed signaling behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/browser/bridge.go`:
- Around line 203-217: The readLoop exit path leaves the websocket open on read
timeout, so the underlying connection can remain allocated. Update
bridgeConn.readLoop to ensure c.close is invoked when the loop exits, either by
adding a deferred close at the start of readLoop or by closing c.ws in the
ReadJSON error path alongside the existing pending-channel cleanup. Keep the fix
localized to bridgeConn and preserve the current closeErr/closed signaling
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d8302bf9-e307-48f2-af81-2028a57a2a04
📒 Files selected for processing (5)
extension/background.jsextension/popup/popup.htmlextension/popup/popup.jsinternal/browser/bridge.gointernal/browser/bridge_test.go
Problem
The jcode Browser Bridge Chrome/Edge extension kept showing "Reconnecting…" with the red "Could not reach the jcode server" error — even though browser tools actually worked. The server log flapped
extension connected/extension disconnectedevery 1–3 minutes for hours.Root causes
1. No server-side keepalive. After the hello handshake the bridge cleared the read deadline and never sent anything. The only thing holding the extension's MV3 service worker awake was its own 30s
chrome.alarmsping, which races the 30s idle-kill (and is clamped to 60s on some builds). When the worker napped, the socket dropped and only reconnected on the next alarm tick — a recurring down-window where the popup shows the scary error.2. Stale-socket state clobber (the "works but shows Reconnecting" bug). All ws event handlers referenced the module-level
ws. A superseded socket's late-firingonclosesetconnected = falseafter a newer socket had already welcomed; the keepalive alarm's reconnect then early-returned because the live globalwswas alreadyOPEN, soconnectedgot stuckfalseforever while the socket kept working.Fix
Server (
internal/browser/bridge.go){"type":"ping"}every 15s — an inbound ws message resets the MV3 worker's idle timer, keeping it (and the whole bridge) alive between commands.writeJSONhelper (gorilla forbids concurrent writers) with a write deadline.Extension (
extension/background.js)sockand guard every handler withws === sock, so a superseded socket can no longer clobber the live connection's state.pongto server pings, and self-healconnectedwhen a ping arrives (receiving one proves we're connected).Popup (
extension/popup/popup.js,popup.html)Test (
internal/browser/bridge_test.go)TestBridgeKeepAlivePingcovers the server ping + read-watchdog drop.Verification
go build ./...,go vet ./internal/browser/, andgo test ./internal/browser/all pass (including the new test)./api/browser/statusreportedextension_online: true.Reviewer notes
chrome://extensions/edge://extensions.keepAlivePing/keepAliveWaitarevar(notconst) so tests can shrink them.Generated with Jack AI bot
Summary by CodeRabbit
New Features
Bug Fixes