How law-chain-hot/websocket-devtools Handles WebSocket Connection States

The extension proxies the native WebSocket constructor to track connection states through a centralized connectionInfo object that transitions from "connecting" → "open" → "closing" → "closed" (or "error"), with the UI reflecting these states in real-time.

The law-chain-hot/websocket-devtools repository is a browser extension that provides debugging capabilities for WebSocket connections. Understanding how it manages WebSocket connection states reveals a sophisticated proxy pattern that intercepts native browser APIs to monitor lifecycle transitions. This article examines the implementation details of how the extension tracks connecting, open, closing, closed, and error states across the content script and UI components.

The State Tracking Architecture

At the core of the extension's state management is a proxy pattern that wraps the browser's native WebSocket constructor. When a page creates a new WebSocket, the injected script in src/content/injected.js intercepts the call and initializes a state tracking object.

Proxying the Native Constructor

The extension replaces the global WebSocket with a ProxiedWebSocket function that instantiates the original native object while simultaneously creating a connectionInfo entry. This entry stores the current status and metadata in a Map called connections.

function ProxiedWebSocket(url, protocols) {
  const connectionId = generateConnectionId();
  const ws = new OriginalWebSocket(url, protocols);

  const connectionInfo = {
    id: connectionId,
    url,
    ws,
    status: "connecting",          // ← initial state
    // … other bookkeeping fields …
  };
  connections.set(connectionId, connectionInfo);

  // Tell the panel we are connecting
  sendEvent({ id: connectionId, type: "connection", status: "connecting", ... });

  // Listen for native events
  ws.addEventListener("open", () => {
    connectionInfo.status = "open";
    sendEvent({ id: connectionId, type: "open", status: "open", ... });
  });

  ws.addEventListener("close", () => {
    connectionInfo.status = "closed";
    sendEvent({ id: connectionId, type: "close", status: "closed", ... });
    connections.delete(connectionId);
  });

  ws.addEventListener("error", () => {
    connectionInfo.status = "error";
    sendEvent({ id: connectionId, type: "error", status: "error", ... });
  });
}

The connectionInfo Object

Each WebSocket connection maintains a connectionInfo object within the connections Map. The status field is the single source of truth for the connection's lifecycle stage. The proxy updates this field immediately when native events fire (open, close, error) or when user actions trigger state changes.

Mapping the Five Connection States

The extension defines five distinct WebSocket connection states, each with specific entry conditions and UI representations.

Connecting

The "connecting" state is assigned immediately upon proxy instantiation. In src/content/injected.js (lines 95‑103, 112‑119), the status is initialized to "connecting" and a system event is emitted to the UI panel. This state indicates the handshake has started but the connection is not yet established.

Open

When the native socket fires its open event, the proxy listener in src/content/injected.js (lines 158‑167) updates connectionInfo.status to "open". This represents a successfully established WebSocket ready for bidirectional communication.

Closing

The "closing" state occurs when the user initiates a disconnect through the DevTools panel. In src/components/WebSocketList.jsx (lines 56‑63), clicking the close button triggers onSetConnectionClosing(id), which sends a simulate-system-event of type client-close to the background script. The injected script receives this message, sets connectionInfo.status = "closing", and emits a system event before the native close handshake completes.

Closed

After the native socket's close event fires (or a simulated close finishes), the proxy updates connectionInfo.status to "closed" (lines 48‑55, 66‑73 in src/content/injected.js). The entry is then removed from the connections Map to prevent memory leaks.

Error

When the native socket fires an error event or a simulated error is injected, the proxy changes connectionInfo.status to "error" (lines 48‑55 in src/content/injected.js). This state indicates the connection failed to establish or encountered a runtime error.

State Transitions in the UI

The React components in src/components/WebSocketList.jsx consume the status field to render appropriate visual feedback and group connections logically.

Visual Indicators

The UI maps each status to specific icons and labels:

const renderStatusIcon = () => {
  if (connection.status === "connecting") return <Loader size={14} color="#f59e0b" />;
  if (connection.status === "closing")   return <Loader size={14} color="#ef4444" />;
  if (isActive)                         return <CheckCircle size={14} color="#10b981" />;
  return <XCircle size={14} color="#ef4444" />;
};

const getStatusText = () => {
  if (connection.status === "connecting") return t("panel.connectionList.status.connecting");
  if (connection.status === "closing")   return t("panel.connectionList.status.closing");
  if (isActive)                         return t("panel.connectionList.status.connected");
  return t("panel.connectionList.status.disconnected");
};
  • Connecting: Yellow spinner with "Connecting" text
  • Open: Green check-mark with "Connected" text
  • Closing: Red spinner with "Closing" text
  • Closed/Error: Red X icon with "Disconnected" or "Error" text

Grouping Active vs Inactive Connections

The UI separates connections into two categories based on status:

  • Active Connections: Status is "open", "connecting", or "closing" (connections that currently exist or are in transition)
  • Inactive Connections: Status is "closed" or "error" (terminated connections shown for historical debugging)

Key Implementation Files

The state management system spans three primary files:

  • src/content/injected.js: Proxies the native WebSocket, maintains the connections Map, updates status on native events, and emits system events to the panel.
  • src/components/WebSocketList.jsx: Reads status from connection objects, renders status indicators, and sends close commands to trigger the "closing" state.
  • src/background/background.js: Routes messages between the UI and content script, forwarding simulate-system-event requests for user-initiated state changes.
  • src/utils/wsHistoryService.js: Persists connection metadata including final status for the history view.

Summary

  • The extension uses a proxy pattern in src/content/injected.js to intercept native WebSocket creation and monitor lifecycle events.
  • Connection states are stored in a connectionInfo object within a Map, with status fields tracking "connecting", "open", "closing", "closed", and "error".
  • State transitions follow the flow: create → connecting → open → closing → closed (with error as a potential branch from connecting or open).
  • The UI in src/components/WebSocketList.jsx maps these states to visual indicators (spinners, check-marks, X icons) and separates active from inactive connections.
  • User-initiated closes trigger a "closing" intermediate state through a message passing system involving the background script.

Frequently Asked Questions

How does the extension detect when a WebSocket connection is established?

When the native WebSocket fires its open event, the proxy listener in src/content/injected.js updates the connectionInfo.status to "open" and emits a system event. The UI receives this event and displays a green check-mark icon with "Connected" status.

What happens to the connection state when a user clicks the close button?

The UI sends a simulate-system-event message of type client-close through src/background/background.js to the content script. The injected script sets the status to "closing" immediately, shows a red spinner in the UI, then transitions to "closed" when the native socket's close event fires.

Where is the connection state data stored during the WebSocket lifecycle?

Each connection's state is stored in the connections Map within src/content/injected.js. The Map keys are unique connection IDs, and the values are connectionInfo objects containing the status field and other metadata. This Map is cleaned up when connections reach the "closed" or "error" state.

Can the extension track WebSocket errors separately from normal closures?

Yes. The proxy specifically listens for native error events in src/content/injected.js and sets connectionInfo.status = "error". This distinguishes error terminations from graceful closures ("closed"), and the UI renders a red X icon with "Error" text rather than "Disconnected".

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →