How the Message Deduplication Mechanism in law-chain-hot/websocket-devtools Prevents Duplicate WebSocket Messages
The message deduplication mechanism in law-chain-hot/websocket-devtools prevents duplicate WebSocket messages by generating unique message IDs in the content script, tracking processed IDs in a Set within the DevTools panel, and silently discarding any incoming events whose IDs have already been recorded.
The WebSocket DevTools extension faces a common browser extension challenge: messages may be emitted multiple times due to frame navigation, script reinjection, or background script retries. The message deduplication mechanism implemented across the content scripts and DevTools panel guarantees that every WebSocket event is captured and displayed exactly once, regardless of how many times it traverses the extension's messaging pipeline.
Unique Message ID Generation
At the heart of the deduplication system lies the generateMessageId function, shared between src/content/content.js and src/content/injected.js, which constructs collision-resistant identifiers before any message leaves the page context.
ID Composition Strategy
The identifier combines multiple entropy sources to ensure global uniqueness across iframes and sessions. As implemented in src/content/content.js#L13-L19:
let messageIdCounter = 0;
function generateMessageId() {
const frameContext = window.location.href; // include iframe context
return `msg_${Date.now()}_${++messageIdCounter}_${Math.random()
.toString(36)
.substr(2, 9)}_${frameContext.length}`;
}
This composition mixes a millisecond timestamp, an auto-incrementing page counter, a random alphanumeric string, and the frame URL length, making ID collisions statistically impossible even under high-frequency message bursts.
Attaching IDs to Outbound Events
Every WebSocket event is stamped with its unique ID before transmission to the background script. For single events, src/content/content.js#L111-L118 demonstrates the attachment:
const messageId = generateMessageId();
const messageWithId = {
type: "websocket-event",
data: event.data.payload,
messageId, // <-- unique ID
timestamp: Date.now(),
source: "content-script",
// … frameContext omitted for brevity
};
chrome.runtime.sendMessage(messageWithId);
For batched transmissions to reduce extension API overhead, the system ensures each array element carries its own ID at src/content/content.js#L70-L78:
const batchWithIds = batch.map(item => ({
...item,
messageId: item.messageId || generateMessageId(),
timestamp: item.timestamp || Date.now(),
source: "content-script",
}));
chrome.runtime.sendMessage({
type: "websocket-event-batch",
data: batchWithIds,
// …
});
By generating IDs at the content script level—closest to the WebSocket source—the mechanism ensures that duplicates created by extension internal retries or cross-frame propagation carry identical identifiers.
Deduplication Logic in the DevTools Panel
The DevTools panel acts as the central authority for tracking which messages have been processed. It maintains a Set data structure for O(1) lookup performance.
Tracking Processed Message IDs
Inside src/devtools/panel.jsx#L55-L58, the panel initializes a persistent reference to store seen identifiers:
const processedMessageIds = useRef(new Set());
This Set persists across React renders, providing stable memory for the deduplication state throughout the debugging session.
Filtering Single Events
When the panel receives a runtime message, it performs an early-exit check to prevent duplicate processing. The logic at src/devtools/panel.jsx#L66-L73 implements this guard:
if (messageId && processedMessageIds.current.has(messageId)) {
// Already seen → ignore
if (hasSendResponse) {
sendResponse({ received: true, duplicate: true, messageId });
}
return;
}
if (messageId) processedMessageIds.current.add(messageId);
If the ID exists in the set, the function returns immediately after optionally signaling the sender. New IDs are added to the set only after confirming they are unique, ensuring subsequent identical messages are filtered out before triggering expensive UI updates.
Handling Batched Events
For batch transmissions, the panel filters the entire array in a single operation. At src/devtools/panel.jsx#L55-L68, the extension iterates through the batch and removes duplicates:
const validEvents = batchEvents.filter(eventData => {
if (eventData.tabId !== tabId) return false;
// Deduplication
if (eventData.messageId && processedMessageIds.current.has(eventData.messageId)) {
return false; // drop duplicate
}
if (eventData.messageId) {
processedMessageIds.current.add(eventData.messageId);
}
return true;
});
Only the filtered validEvents array proceeds to state updates, preventing duplicate entries from ever reaching the React render cycle or the message history display.
Memory Management and Cleanup
To prevent unbounded growth of the processedMessageIds set during long debugging sessions, the extension implements a circular buffer strategy. When the set exceeds MAX_PROCESSED_IDS, the cleanup logic at src/devtools/panel.jsx#L44-L48 truncates the oldest entries:
if (processedMessageIds.current.size > MAX_PROCESSED_IDS) {
const idsArray = Array.from(processedMessageIds.current);
processedMessageIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS / 2));
}
This approach retains the most recent half of the processed IDs, maintaining deduplication accuracy for active traffic while freeing memory from ancient message history.
Why This Design Works
The message deduplication mechanism succeeds through four key architectural decisions:
-
Origin-time uniqueness: By generating IDs immediately upon WebSocket event capture using timestamp, counter, random seed, and frame context, the system guarantees that every distinct network packet receives a distinct identifier, even if the same logical message is retransmitted by the browser.
-
Centralized authority: The DevTools panel serves as the single source of truth for processed messages, preventing race conditions that would occur if deduplication were distributed across background scripts or content scripts.
-
Early discard: Duplicate detection happens before any React state mutations or UI reconciliation, preserving frame rates during high-volume WebSocket traffic such as real-time gaming or financial tickers.
-
Memory bounding: The
MAX_PROCESSED_IDSlimit ensures that the extension remains performant during multi-hour debugging sessions without leaking memory.
Practical Implementation Examples
Simulating WebSocket Messages
When injecting custom messages from the DevTools panel, the system automatically generates fresh IDs:
// In the DevTools panel (e.g. from a button)
chrome.runtime.sendMessage({
type: "simulate-message",
connectionId: selectedConnectionId,
message: "Hello from DevTools!",
direction: "incoming",
}); // The injected script will generate its own messageId
The panel receives the simulated event, checks it against processedMessageIds, and displays it exactly once.
Testing Deduplication Behavior
To verify the mechanism works correctly, you can simulate rapid duplicate transmission:
// Simulate two identical events arriving back-to-back
const dup = {
messageId: "msg_12345_1_abcde",
tabId: 1,
type: "message",
data: "test"
};
chrome.runtime.sendMessage(dup); // first – processed and added to UI
chrome.runtime.sendMessage(dup); // second – detected as duplicate, silently dropped
Only the first call increments message counters or appears in the inspection list.
Summary
- The
generateMessageIdfunction insrc/content/content.jsandsrc/content/injected.jscreates unique identifiers using timestamps, counters, random values, and frame context. - Every WebSocket event—whether single or batched—is stamped with a
messageIdbefore leaving the content script. - The DevTools panel maintains a
processedMessageIdsSet to track previously handled messages with O(1) lookup efficiency. - Incoming messages are filtered against this Set, with duplicates discarded before any UI updates occur.
- Memory usage remains bounded through periodic cleanup that retains only the most recent
MAX_PROCESSED_IDSentries.
Frequently Asked Questions
How does the extension handle message deduplication across browser tabs?
The message deduplication mechanism operates primarily within individual DevTools panels, each tracking its own processedMessageIds Set. While the content script generates IDs using window.location.href to ensure uniqueness across iframes within a tab, cross-tab deduplication relies on the per-tab isolation of the DevTools panel state. Each tab maintains independent tracking sets, preventing cross-contamination while ensuring local consistency.
What prevents the processed message ID set from consuming all available memory?
The extension implements automatic memory management through a hard limit defined by MAX_PROCESSED_IDS. When the processedMessageIds Set exceeds this threshold, the cleanup logic in src/devtools/panel.jsx removes the oldest half of the entries, keeping memory usage predictable during extended debugging sessions without sacrificing deduplication accuracy for recent messages.
Can duplicate messages arrive from different sources within the same extension?
Yes, duplicates can originate from content script reinjection, background script message retries, or batched vs. individual transmission paths. The message deduplication mechanism neutralizes these sources by standardizing on the messageId generated at the earliest point of interception. Regardless of which internal extension path delivers the message, identical IDs are detected and filtered by the central Set in the DevTools panel.
Why use a Set instead of an array for tracking processed IDs?
The DevTools panel uses a JavaScript Set rather than an array because Set operations provide O(1) average time complexity for both insertion and membership testing via has(). In contrast, array-based tracking would require O(n) linear scans for each incoming message, creating performance bottlenecks when handling high-frequency WebSocket streams containing thousands of messages per minute.
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 →