Real-Time Slashing Event Feed Duplicate Display Under WebSocket Reconnection
Problem Statement
The slashing event feed in src/components/slashing/SlashingEventFeed.tsx displays real-time slashing events as they occur. The useSlashingStream() hook at src/hooks/useSlashingStream.ts:55 subscribes to a WebSocket channel that emits SlashingEvent messages. When the WebSocket reconnects (after a brief network interruption), the server re-sends the last 5 events as a "catch-up" burst (to ensure the client doesn't miss events during disconnection). The hook's onMessage handler at line 70 pushes each event into a useState<SlashingEvent[]> list. If the client was disconnected for 2 seconds and 3 slashing events occurred during that time, the catch-up burst includes those 3 events. However, the client may have already received some of these events before disconnection (partial overlap). The client processes the catch-up events without deduplication, adding the same 3 events again. The feed shows 6 events instead of 3. Duplicate slashing events in the feed cause the NOC operator to believe more nodes are being slashed than actually are, potentially triggering an unnecessary emergency response.
State Invariants & Parameters
- WebSocket reconnection: brief (50ms-5s)
- Catch-up burst size: 5 events
- Max events during disconnection: up to 5 (covered by catch-up)
- Partial overlap probability: high (events received before disconnect)
- Feed display: chronological list with
event.id as key
- Invariant:
∀ event_id: count(feed_events[event_id]) <= 1
Affected Code Paths
src/hooks/useSlashingStream.ts:50-90 — onMessage handler with no dedup
src/hooks/useWebSocketReconnect.ts:40-65 — Reconnection logic with catch-up
src/components/slashing/SlashingEventFeed.tsx:45-75 — Feed rendering
src/hooks/tests/useSlashingStream.test.ts — No reconnection dedup test
Resolution Blueprint
- Add a received event ID set in
useSlashingStream.ts: maintain a Set<eventId> of recently received event IDs (with TTL of 5 minutes via Map<eventId, timestamp> and periodic cleanup). Before adding an event to the feed, check if eventId ∈ receivedIds. If yes, skip. Add the eventId to the set.
- Implement server-side dedup headers: the catch-up burst includes a
x-catchup-from header with the last event ID the server has. The client sends its last received event ID in the x-last-event-id header on reconnect. The server only sends events with id > client_last_event_id. This eliminates the partial overlap entirely.
- Use a keyed React render with
event.id as the key prop: React's reconciliation will not re-render duplicate keys. However, the event will still be in the array (just not rendered visually). To remove it, filter the array on insertion: setEvents(prev => prev.some(e => e.id === event.id) ? prev : [...prev, event]).
- In the feed component, add an invisible dedup layer: a
useRef<Set<string>> tracks displayed event IDs. Before rendering, filter out any event whose ID is already displayed. The dedup layer is reset only on full page navigation.
- Add a WebSocket reconnection e2e test that simulates a 2-second disconnect during which 3 slashing events are emitted, then reconnects, and verifies exactly 3 unique events appear in the feed (no duplicates).
Labels
Complexity: Hardcore
Layer: Core-Engine
Type: Race-Condition
Real-Time Slashing Event Feed Duplicate Display Under WebSocket Reconnection
Problem Statement
The slashing event feed in
src/components/slashing/SlashingEventFeed.tsxdisplays real-time slashing events as they occur. TheuseSlashingStream()hook atsrc/hooks/useSlashingStream.ts:55subscribes to a WebSocket channel that emitsSlashingEventmessages. When the WebSocket reconnects (after a brief network interruption), the server re-sends the last 5 events as a "catch-up" burst (to ensure the client doesn't miss events during disconnection). The hook'sonMessagehandler at line 70 pushes each event into auseState<SlashingEvent[]>list. If the client was disconnected for 2 seconds and 3 slashing events occurred during that time, the catch-up burst includes those 3 events. However, the client may have already received some of these events before disconnection (partial overlap). The client processes the catch-up events without deduplication, adding the same 3 events again. The feed shows 6 events instead of 3. Duplicate slashing events in the feed cause the NOC operator to believe more nodes are being slashed than actually are, potentially triggering an unnecessary emergency response.State Invariants & Parameters
event.idas key∀ event_id: count(feed_events[event_id]) <= 1Affected Code Paths
src/hooks/useSlashingStream.ts:50-90—onMessagehandler with no dedupsrc/hooks/useWebSocketReconnect.ts:40-65— Reconnection logic with catch-upsrc/components/slashing/SlashingEventFeed.tsx:45-75— Feed renderingsrc/hooks/tests/useSlashingStream.test.ts— No reconnection dedup testResolution Blueprint
useSlashingStream.ts: maintain aSet<eventId>of recently received event IDs (with TTL of 5 minutes viaMap<eventId, timestamp>and periodic cleanup). Before adding an event to the feed, check ifeventId ∈ receivedIds. If yes, skip. Add the eventId to the set.x-catchup-fromheader with the last event ID the server has. The client sends its last received event ID in thex-last-event-idheader on reconnect. The server only sends events withid > client_last_event_id. This eliminates the partial overlap entirely.event.idas thekeyprop: React's reconciliation will not re-render duplicate keys. However, the event will still be in the array (just not rendered visually). To remove it, filter the array on insertion:setEvents(prev => prev.some(e => e.id === event.id) ? prev : [...prev, event]).useRef<Set<string>>tracks displayed event IDs. Before rendering, filter out any event whose ID is already displayed. The dedup layer is reset only on full page navigation.Labels
Complexity: HardcoreLayer: Core-EngineType: Race-Condition