How the processedMessageIds Set Prevents Duplicate WebSocket Messages in law-chain-hot/websocket-devtools

The processedMessageIds Set is an in-memory deduplication cache that ensures each WebSocket message appears exactly once in the DevTools UI by tracking unique message IDs generated for every intercepted send or receive event.

The processedMessageIds Set is a critical component of the law-chain-hot/websocket-devtools extension that solves the duplicate-message problem inherent in WebSocket proxy implementations. When the extension patches the native WebSocket constructor to intercept traffic in src/content/injected.js, both the page script and the proxy script can fire identical events, which would clutter the DevTools panel with redundant entries. This article explains how the Set eliminates duplicates and how its lifecycle is managed across page navigations and extension state changes.

Why processedMessageIds Exists

When the proxy script replaces the native WebSocket constructor, it intercepts every incoming message event and outgoing send call. Because both the original page context and the injected proxy can trigger the same WebSocket events, the extension risks processing identical data multiple times. Without deduplication, the DevTools UI would display duplicate entries for every message sent or received.

The processedMessageIds Set solves this by storing the unique messageId (a UUID generated by generateMessageId()) for each processed message. Before logging or forwarding any message to the background page, the proxy checks processedMessageIds.has(id). If the ID exists, the message is ignored; otherwise, it is added to the Set and processed normally. This guarantees that each WebSocket message appears once in the inspection panel, regardless of how many times the proxy intercepts the underlying event.

How State is Managed

The lifecycle of the processedMessageIds Set follows a strict creation-population-cleanup pattern to prevent memory leaks and ensure data integrity.

Initialization in the Content Script

The Set is instantiated when the injected script first executes on the target page. In src/content/injected.js, the initialization occurs at the module level:

const processedMessageIds = new Set();

This creates a singleton instance that persists for the lifetime of the page context, making it available to all intercepted WebSocket instances.

Deduplication Logic

Every captured message receives a unique identifier via generateMessageId(), which produces UUIDs in the format msg_xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx. The proxy validates each message against the Set before processing:

const ourMessageListener = event => {
  const id = generateMessageId();

  // Skip if we have already processed this message
  if (processedMessageIds.has(id)) return;

  processedMessageIds.add(id);
  const binaryInfo = processMessageWithBinary(event.data);
  
  sendEvent({
    id,
    data: event.data,
    ...binaryInfo,
    messageId: id,
  });
};

This check occurs for both inbound messages (via the message event listener) and outbound messages (via the patched send method), ensuring complete deduplication across the entire WebSocket traffic flow.

Cleanup and Memory Management

The Set is cleared in three specific scenarios to avoid unbounded memory growth and ensure a fresh state after page changes:

  • Navigation and Page Refresh: When the user navigates away or reloads, the beforeunload event triggers a complete wipe of the Set:

    window.addEventListener('beforeunload', () => processedMessageIds.clear());
  • Explicit Proxy Reset: When the DevTools panel sends a reset-proxy-state command, the background script (src/background/background.js) forwards this instruction to the content script, which empties the Set immediately. This allows developers to clear the message history without reloading the page.

  • Extension Lifecycle Events: Chrome's onSuspend event triggers a cleanup routine that clears stored IDs when the extension is unloaded or suspended, preventing stale data from persisting across browser sessions.

Implementation Details in src/content/injected.js

The core deduplication logic resides in src/content/injected.js, where the Set coordinates with the message interception mechanism. The UUID generation function ensures collision-resistant identifiers:

function generateMessageId() {
  return 'msg_' + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = Math.random() * 16 | 0;
    const v = (c === 'x') ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

The extension stores the messageId within the event payload sent to src/background/background.js, allowing the DevTools panel to correlate messages with their processed status while maintaining the deduplication guarantee at the source.

Summary

  • The processedMessageIds Set acts as an in-memory filter to prevent duplicate WebSocket messages from appearing in the DevTools UI.
  • It is instantiated in src/content/injected.js and tracks UUIDs generated by generateMessageId() for every intercepted message.
  • Deduplication occurs via processedMessageIds.has(id) checks before processing any message event or send call.
  • The Set is cleared on page navigation via beforeunload, explicitly via reset-proxy-state commands, and during extension suspension via Chrome lifecycle events.
  • This mechanism ensures accurate, duplicate-free WebSocket inspection even when the proxy intercepts the same raw data multiple times.

Frequently Asked Questions

How does processedMessageIds prevent memory leaks during long browsing sessions?

The Set implements aggressive cleanup strategies to bound memory usage. It clears automatically on beforeunload events during navigation, responds to explicit reset-proxy-state commands from the DevTools panel, and wipes data when Chrome suspends the extension via onSuspend. These three mechanisms ensure that message IDs do not accumulate indefinitely as users browse between pages.

What format does the messageId use and why?

The messageId uses a UUID v4 format prefixed with msg_, generated by the generateMessageId() function in src/content/injected.js. This format provides sufficient randomness (122 bits of entropy) to prevent collisions between messages while maintaining a consistent string structure that can be easily indexed by the JavaScript Set and transmitted to the background script for UI correlation.

Can processedMessageIds survive page reloads or navigation?

No, the processedMessageIds Set is an in-memory data structure tied to the specific JavaScript execution context of the page. When navigation occurs, the beforeunload handler explicitly calls processedMessageIds.clear(), and the entire content script context is destroyed by the browser. A fresh Set is created when the injected script runs on the new page, ensuring no stale message IDs persist across different URLs.

How does the background script coordinate with processedMessageIds?

While the Set itself lives in the content script (src/content/injected.js), the background script in src/background/background.js propagates control signals like reset-proxy-state. When the DevTools panel requests a state reset, the background forwards this message to the content script, which then executes processedMessageIds.clear() to reset the deduplication cache without requiring a page refresh.

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 →