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
beforeunloadevent triggers a complete wipe of the Set:window.addEventListener('beforeunload', () => processedMessageIds.clear()); -
Explicit Proxy Reset: When the DevTools panel sends a
reset-proxy-statecommand, 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
onSuspendevent 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
processedMessageIdsSet acts as an in-memory filter to prevent duplicate WebSocket messages from appearing in the DevTools UI. - It is instantiated in
src/content/injected.jsand tracks UUIDs generated bygenerateMessageId()for every intercepted message. - Deduplication occurs via
processedMessageIds.has(id)checks before processing anymessageevent orsendcall. - The Set is cleared on page navigation via
beforeunload, explicitly viareset-proxy-statecommands, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →