Skip to content

feat(ws): backend heartbeat in WebSocketCommunicator + useWebSocketReconnect React hook - #634

Open
AseemPrasad wants to merge 4 commits into
abi:mainfrom
AseemPrasad:asmstc
Open

feat(ws): backend heartbeat in WebSocketCommunicator + useWebSocketReconnect React hook #634
AseemPrasad wants to merge 4 commits into
abi:mainfrom
AseemPrasad:asmstc

Conversation

@AseemPrasad

Copy link
Copy Markdown

Backend: backend/routes/generate_code.py

The WebSocketCommunicator class now maintains a keepalive heartbeat for every active WebSocket connection:

  • Added PING_INTERVAL_SECONDS = 25.0 as a class constant. This interval is deliberately below the 30-second
    timeout commonly enforced by corporate proxies, ensuring the connection is kept alive before the proxy kills
    it.
  • Added _heartbeat_task: asyncio.Task[None] | None instance field to track the background heartbeat coroutine.
  • accept() now starts the heartbeat task immediately after the connection is accepted, so the keepalive begins
    before any user code runs.
  • Added _run_heartbeat() — a background asyncio.Task that sends an empty TEXT frame every 25 seconds. The
    empty frame exercises the TCP connection (triggering a TCP keepalive probe at the transport layer) without
    adding protocol-level noise. When Starlette is upgraded to ≥ 0.40 (which exposes a real websocket.ping()
    API), the implementation should switch to that.
  • Added _cleanup() — stops the heartbeat task via .cancel() and marks self.is_closed = True. This replaces the
    repeated inline cancellation logic that was duplicated across close() and throw_error().
  • close() now calls await self._cleanup() after closing, rather than just setting the flag. This guarantees
    the heartbeat task is cancelled on normal shutdown.
  • throw_error() also calls await self._cleanup() when closing after an application-level error, so the
    heartbeat is cancelled even on the error path.

The heartbeat is intentionally fire-and-forget from the pipeline's perspective — the pipeline code is
unchanged; the WebSocketCommunicator encapsulates the lifecycle.

────────────────────────────────────────────────────────────────────────────────

Frontend: frontend/src/lib/useWebSocketReconnect.ts (new file)

A general-purpose React hook for WebSocket connections that need automatic reconnection:

API surface:

  const { status, retryCount, send, reconnect, disconnect } = useWebSocketReconnect({                          
    url: "ws://localhost:7001/generate-code",                                                                  
    maxRetries: 10,        // default: 10; gives up after 10 failed attempts                                   
    baseDelayMs: 1000,    // default: 1000; exponential base                                                   
    maxDelayMs: 30_000,   // default: 30_000; cap at 30 s                                                      
    onDisconnect: () => setDisconnected(true),                                                                 
    onConnect: () => setDisconnected(false),                                                                   
  });                                                                                                          

status — one of "disconnected" | "connecting" | "connected". Callers can use this to render a "Reconnecting…"
banner or disable the send button.

send(data) — wraps JSON.stringify and sends only when readyState === OPEN. Safe to call in any render phase; a
no-op when the socket is not open.

reconnect() — tears down the current socket and retries immediately. Intended for use by a "Retry now" UI
button after all automatic retries are exhausted.

disconnect() — permanently closes the socket and stops all timers. Use this when the user navigates away or
explicitly cancels a generation.

Reconnection strategy:

  • Exponential back-off: 1 s → 2 s → 4 s → … → 30 s cap
  • ±500 ms jitter added to each delay to prevent thundering-herd when multiple clients reconnect simultaneously
  • After maxRetries attempts, the hook stops and fires onDisconnect

onDisconnect / onConnect callbacks allow the host component to persist generation state (prompt, file content)
before retrying — a prerequisite for the state-resume feature (tracked separately in PR 5's backlog).

────────────────────────────────────────────────────────────────────────────────

What Is NOT Included in This PR

The following were in the original PR 5 proposal and are intentionally deferred to a follow-up:

  1. State resume protocol — Persisting and replaying file_state, prompt_messages, and variant_index across
    reconnects requires a protocol extension ("resumeState" message type) and agent snapshot logic. The
    useWebSocketReconnect hook is designed to call resumeState once the protocol is defined.

  2. Frontend integration — Wiring useWebSocketReconnect into the main generateCode flow requires careful
    coordination with the existing project-store state machine. That integration is safer to do as a dedicated
    follow-up PR once the heartbeat-only changes have been in production.

  3. Frontend reconnection UI — A "Connection lost — retrying (3/10)…" banner and "Give up" / "Retry now"
    controls are deferred to the integration PR.

────────────────────────────────────────────────────────────────────────────────

Rollout Notes

  • The backend heartbeat is backward-compatible — existing clients continue to work without modification. The
    heartbeat is invisible to the client.
  • SCREENSHOT_CACHE_ENABLED defaults to true in config.py, so existing deployments get screenshot caching for
    free. Set SCREENSHOT_CACHE_ENABLED=0 in .env to disable.
  • useWebSocketReconnect is opt-in — it is not yet wired into the main app. Consuming components must
    explicitly use it.

────────────────────────────────────────────────────────────────────────────────

Files Changed

┌──────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐
│ File │ Change │
├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ backend/routes/generate_code.py │ WebSocketCommunicator heartbeat (accept/close/throw_error │
│ │ paths) │
├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ frontend/src/lib/useWebSocketReconnect.t │ New — reconnect hook with back-off, status, │
│ s │ send/reconnect/disconnect │
└──────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘

@AseemPrasad

Copy link
Copy Markdown
Author

@abi
would love to get this contribution reviewed and incorporated..
Thank you..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants