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

> Discover how the processedMessageIds Set in websocket-devtools prevents duplicate WebSocket messages in the UI. Learn about its in-memory deduplication cache and state management.

- Repository: [Brian 阿布/websocket-devtools](https://github.com/law-chain-hot/websocket-devtools)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js), the initialization occurs at the module level:

```javascript
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:

```javascript
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:

  ```javascript
  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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js), where the Set coordinates with the message interception mechanism. The UUID generation function ensures collision-resistant identifiers:

```javascript
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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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`](https://github.com/law-chain-hot/websocket-devtools/blob/main/src/content/injected.js)), the background script in [`src/background/background.js`](https://github.com/law-chain-hot/websocket-devtools/blob/main/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.