feat: implement JWT-authenticated WebSocket event system with Redis Pub/Sub scaling#261
feat: implement JWT-authenticated WebSocket event system with Redis Pub/Sub scaling#261onajidavid87-web wants to merge 1 commit into
Conversation
…th Redis Pub/Sub scaling Implemented a scalable real-time WebSocket event system using Express and Socket.IO with JWT authentication, Redis Pub/Sub horizontal scaling, and namespace-based routing (/alerts, /scores, /transactions). Included a resilient React WebSocketProvider with offline message queuing, custom hooks (useWebSocket, useWebSocketEvent), event deduplication, and integration stress testing for 100+ concurrent connections under 100ms latency. Closes nexoraorg#259
📝 WalkthroughWalkthroughChangesWebSocket infrastructure
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
backend/package.json (1)
63-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove
@types/socket.iodependency.Socket.IO v3 and v4 include their own TypeScript definitions out of the box. The
@types/socket.iopackage is deprecated for these versions and may cause type conflicts or masking of the official types.♻️ Proposed fix
- "`@types/socket.io`": "^3.0.2",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/package.json` at line 63, Remove the deprecated `@types/socket.io` entry from backend/package.json dependencies, leaving the official Socket.IO package to provide its bundled TypeScript definitions.frontend/src/components/WebSocketProvider.tsx (2)
196-206: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
reconnectTimerRefappears to be dead code. It is declared (Line 76) and only ever cleared here; it is never assigned a timer. Socket.IO's built-in reconnection is already enabled, so either wire this into a manual backoff path or remove it to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/WebSocketProvider.tsx` around lines 196 - 206, Remove the unused reconnectTimerRef declaration and its clearTimeout cleanup from disconnect, since no manual reconnect timer is assigned and Socket.IO handles reconnection. Keep the socket cleanup, status update, and reconnect-attempt reset in disconnect unchanged.
217-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
String.prototype.substris deprecated. Useslice(2, 11)(orcrypto.randomUUID()where available) instead.♻️ Suggested change
- id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/WebSocketProvider.tsx` at line 217, Update the message ID generation expression near the WebSocket provider’s message construction to replace deprecated String.prototype.substr with slice(2, 11), preserving the existing random suffix length and ID format.frontend/src/components/__tests__/WebSocketProvider.test.tsx (1)
16-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMock's single-slot handler capture makes the
useWebSocketEventtest order-dependent, not a real isolation test.The mock's
on()tracks only the most-recently-registered handler per event name (connectHandler,alertHandler) across all mocked socket instances. SinceWebSocketProvideritself registers an internal'alert:new'listener for/alerts(per its source) in addition to whateveruseWebSocketEvent'ssubscribe()registers,alertHandlerin this test is bound to whichever registration happens to run last — an internal implementation detail, not something the test explicitly controls. The test's intent ("receives events via useWebSocketEvent hook") isn't reliably isolated from the provider's own built-in handling.Consider tracking registrations by call index/socket instance (e.g., store all
oncalls in an array and invoke handlers by matching event name explicitly per assertion) rather than relying on a single mutable capture variable per event name.Also applies to: 94-108
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/__tests__/WebSocketProvider.test.tsx` around lines 16 - 33, Make the socket.io mock in WebSocketProvider tests capture every on registration per socket instance or call rather than overwriting shared connectHandler and alertHandler variables. Update the useWebSocketEvent assertions to select and invoke the intended alert:new registration explicitly, keeping the provider’s internal listener separate so the test does not depend on registration order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/websocket/index.ts`:
- Around line 126-132: Restrict the subscribe handler so clients cannot join
arbitrary rooms. Validate the requested room against the authenticated user’s
identity and roles using the existing authorization context, and call
socket.join only for permitted rooms; otherwise reject or ignore the request
without emitting subscribed. Update the socket.on('subscribe') flow and preserve
server-controlled room membership.
In `@frontend/src/components/WebSocketProvider.tsx`:
- Around line 79-90: The shared seenEventIdsRef incorrectly suppresses identical
events across independent consumers; scope deduplication to each
subscription/handler or restrict it to the provider’s internal recent* state
updates. Update frontend/src/components/WebSocketProvider.tsx lines 79-90
accordingly; frontend/src/hooks/useWebSocketEvent.ts lines 23-34 requires no
direct change and should receive events once the provider no longer shares
global dedup state.
- Around line 113-193: Prevent connect’s identity from changing when consumers
provide inline callbacks by storing onConnectionChange, onError, onAlert,
onScoreUpdate, and onTransaction in refs and invoking the current ref values
inside the socket handlers. Update the connect dependency list to use those
stable refs while preserving callback behavior, so the auto-connect effect does
not repeatedly disconnect and recreate sockets on parent renders.
- Around line 229-247: Update subscribe and the socket lifecycle to maintain a
ref-backed registry of namespace/event/callback subscriptions instead of binding
only to the socket returned at call time. Ensure subscriptions registered before
a socket exists are bound when that namespace socket is created, and existing
subscriptions are detached from replaced sockets and reattached to the new
instances during connect/reconnect, while preserving duplicate-event filtering
and unsubscribe behavior.
- Around line 132-160: Update the WebSocketProvider connection handlers to track
status, reconnect attempts, and errors independently for each namespace/socket
rather than overwriting shared state. Derive the exposed aggregate status and
onConnectionChange notifications from all required namespaces, so one namespace
disconnecting or reconnecting does not report the whole connection as
disconnected while others remain connected. Keep connect events scoped to their
namespace and only reset aggregate reconnect state when no namespace is still
reconnecting.
In `@tests/integration/websocket.integration.test.ts`:
- Line 83: Update the alert client filtering around connectedSockets to avoid
the private socket.nsp field. Preserve each socket’s namespace string when
creating the clients and filter using that stored namespace, selecting entries
whose namespace ends with “/alerts”.
---
Nitpick comments:
In `@backend/package.json`:
- Line 63: Remove the deprecated `@types/socket.io` entry from
backend/package.json dependencies, leaving the official Socket.IO package to
provide its bundled TypeScript definitions.
In `@frontend/src/components/__tests__/WebSocketProvider.test.tsx`:
- Around line 16-33: Make the socket.io mock in WebSocketProvider tests capture
every on registration per socket instance or call rather than overwriting shared
connectHandler and alertHandler variables. Update the useWebSocketEvent
assertions to select and invoke the intended alert:new registration explicitly,
keeping the provider’s internal listener separate so the test does not depend on
registration order.
In `@frontend/src/components/WebSocketProvider.tsx`:
- Around line 196-206: Remove the unused reconnectTimerRef declaration and its
clearTimeout cleanup from disconnect, since no manual reconnect timer is
assigned and Socket.IO handles reconnection. Keep the socket cleanup, status
update, and reconnect-attempt reset in disconnect unchanged.
- Line 217: Update the message ID generation expression near the WebSocket
provider’s message construction to replace deprecated String.prototype.substr
with slice(2, 11), preserving the existing random suffix length and ID format.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 119863b2-0d7b-4e23-83d7-c62e3aedd364
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
backend/docker-compose.ws-scale.ymlbackend/package.jsonbackend/src/__tests__/websocket.test.tsbackend/src/index.tsbackend/src/services/websocketService.tsbackend/src/websocket/index.tsdocs/websocket-architecture.mdfrontend/package.jsonfrontend/src/components/WebSocketProvider.tsxfrontend/src/components/__tests__/WebSocketProvider.test.tsxfrontend/src/hooks/index.tsfrontend/src/hooks/useWebSocket.tsfrontend/src/hooks/useWebSocketEvent.tsfrontend/src/types/websocket.tstests/integration/package.jsontests/integration/websocket.integration.test.ts
| // Handle explicit room join requests | ||
| socket.on('subscribe', (room: string) => { | ||
| if (typeof room === 'string' && room.trim()) { | ||
| socket.join(room); | ||
| socket.emit('subscribed', { room }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Insecure room joining allows clients to intercept private messages.
Allowing clients to join arbitrary rooms via the subscribe event enables any user to join sensitive rooms (e.g., user:<other_user_id> or role:admin) and intercept private broadcasts.
Room membership should be controlled strictly by the server based on the user's authenticated identity and roles, or the requested room must be validated against the user's permissions before calling socket.join(room).
🔒️ Proposed fix to restrict room subscriptions
// Handle explicit room join requests
socket.on('subscribe', (room: string) => {
if (typeof room === 'string' && room.trim()) {
- socket.join(room);
- socket.emit('subscribed', { room });
+ // Example validation: only allow joining public rooms or explicitly authorized rooms
+ if (room.startsWith('public:')) {
+ socket.join(room);
+ socket.emit('subscribed', { room });
+ } else {
+ socket.emit('error', { message: 'Unauthorized room subscription' });
+ }
}
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle explicit room join requests | |
| socket.on('subscribe', (room: string) => { | |
| if (typeof room === 'string' && room.trim()) { | |
| socket.join(room); | |
| socket.emit('subscribed', { room }); | |
| } | |
| }); | |
| // Handle explicit room join requests | |
| socket.on('subscribe', (room: string) => { | |
| if (typeof room === 'string' && room.trim()) { | |
| // Example validation: only allow joining public rooms or explicitly authorized rooms | |
| if (room.startsWith('public:')) { | |
| socket.join(room); | |
| socket.emit('subscribed', { room }); | |
| } else { | |
| socket.emit('error', { message: 'Unauthorized room subscription' }); | |
| } | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/websocket/index.ts` around lines 126 - 132, Restrict the
subscribe handler so clients cannot join arbitrary rooms. Validate the requested
room against the authenticated user’s identity and roles using the existing
authorization context, and call socket.join only for permitted rooms; otherwise
reject or ignore the request without emitting subscribed. Update the
socket.on('subscribe') flow and preserve server-controlled room membership.
| const isDuplicateEvent = useCallback((eventId?: string, data?: any): boolean => { | ||
| const id = eventId || (data?.id ? String(data.id) : `${data?.timestamp}-${JSON.stringify(data).slice(0, 30)}`); | ||
| if (seenEventIdsRef.current.has(id)) { | ||
| return true; | ||
| } | ||
| seenEventIdsRef.current.add(id); | ||
| if (seenEventIdsRef.current.size > 200) { | ||
| const firstKey = seenEventIdsRef.current.values().next().value; | ||
| if (firstKey) seenEventIdsRef.current.delete(firstKey); | ||
| } | ||
| return false; | ||
| }, []); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Shared dedup set suppresses events across independent consumers. A single seenEventIdsRef is consulted by both the provider's internal namespace handlers and every subscribe/useWebSocketEvent wrapper, so once any consumer marks an event id as seen, all other consumers of the same event drop it.
frontend/src/components/WebSocketProvider.tsx#L79-L90: scope dedup per subscription/handler (or dedup only for the internalrecent*state updates), not a single global set consulted by every consumer.frontend/src/hooks/useWebSocketEvent.ts#L23-L34: once the provider stops sharing dedup state across consumers, this hook's callback will actually receive events instead of being treated as a duplicate of the provider's internal handler.
📍 Affects 2 files
frontend/src/components/WebSocketProvider.tsx#L79-L90(this comment)frontend/src/hooks/useWebSocketEvent.ts#L23-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/WebSocketProvider.tsx` around lines 79 - 90, The
shared seenEventIdsRef incorrectly suppresses identical events across
independent consumers; scope deduplication to each subscription/handler or
restrict it to the provider’s internal recent* state updates. Update
frontend/src/components/WebSocketProvider.tsx lines 79-90 accordingly;
frontend/src/hooks/useWebSocketEvent.ts lines 23-34 requires no direct change
and should receive events once the provider no longer shares global dedup state.
| const connect = useCallback(async (authToken?: string) => { | ||
| const jwtToken = authToken || token || (typeof window !== 'undefined' ? localStorage.getItem('token') : null) || ''; | ||
|
|
||
| setStatus('connecting'); | ||
|
|
||
| // Connect to monitoring system | ||
| const connect = useCallback(async () => { | ||
| if (monitorRef.current) { | ||
| return; | ||
| } | ||
| NAMESPACES.forEach(ns => { | ||
| if (socketsRef.current.has(ns)) { | ||
| socketsRef.current.get(ns)?.disconnect(); | ||
| } | ||
|
|
||
| try { | ||
| const monitor = initializeMonitor(); | ||
| await monitor.start(); | ||
|
|
||
| // Get initial metrics | ||
| const dashboardData = await monitor.getDashboardData(); | ||
| setMetrics(dashboardData.overview.realTimeMetrics); | ||
|
|
||
| } catch (error) { | ||
| setState(prev => ({ | ||
| ...prev, | ||
| lastError: error instanceof Error ? error.message : t('errors.connectionError') | ||
| })); | ||
| throw error; | ||
| } | ||
| }, [initializeMonitor]); | ||
| const socket = SocketIOClient(`${url}${ns}`, { | ||
| auth: { token: jwtToken }, | ||
| transports: ['websocket', 'polling'], | ||
| reconnection: true, | ||
| reconnectionAttempts: maxReconnectAttempts, | ||
| reconnectionDelay: reconnectInterval, | ||
| autoConnect: true | ||
| }); | ||
|
|
||
| // Disconnect from monitoring system | ||
| const disconnect = useCallback(async () => { | ||
| if (reconnectTimeoutRef.current) { | ||
| clearTimeout(reconnectTimeoutRef.current); | ||
| reconnectTimeoutRef.current = null; | ||
| } | ||
| socket.on('connect', () => { | ||
| setStatus('connected'); | ||
| setLastConnected(new Date()); | ||
| setLastError(undefined); | ||
| setReconnectAttempts(0); | ||
| onConnectionChange?.('connected'); | ||
| flushQueue(); | ||
| }); | ||
|
|
||
| if (monitorRef.current) { | ||
| await monitorRef.current.stop(); | ||
| monitorRef.current = null; | ||
| socket.on('connect_error', (err: Error) => { | ||
| setLastError(err.message); | ||
| setStatus('error'); | ||
| onError?.(err); | ||
| }); | ||
|
|
||
| socket.on('reconnect_attempt', (attempt: number) => { | ||
| setStatus('reconnecting'); | ||
| setReconnectAttempts(attempt); | ||
| onConnectionChange?.('reconnecting'); | ||
| }); | ||
|
|
||
| socket.on('disconnect', (reason: string) => { | ||
| if (reason === 'io server disconnect') { | ||
| // Severed by server, try manual reconnect | ||
| socket.connect(); | ||
| } | ||
| setStatus('disconnected'); | ||
| onConnectionChange?.('disconnected'); | ||
| }); | ||
|
|
||
| // Register Namespace Specific Real-Time Event Handlers | ||
| if (ns === '/alerts') { | ||
| socket.on('alert:new', (alert: FraudAlertEvent) => { | ||
| if (isDuplicateEvent(alert.id, alert)) return; | ||
| setRecentAlerts(prev => [alert, ...prev.slice(0, 19)]); | ||
| onAlert?.(alert); | ||
| }); | ||
| } | ||
|
|
||
| if (ns === '/scores') { | ||
| socket.on('score:updated', (score: CreditScoreEvent) => { | ||
| if (isDuplicateEvent(undefined, score)) return; | ||
| setRecentScoreUpdates(prev => [score, ...prev.slice(0, 19)]); | ||
| onScoreUpdate?.(score); | ||
| }); | ||
| } | ||
|
|
||
| if (ns === '/transactions') { | ||
| socket.on('transaction:new', (tx: TransactionEventPayload) => { | ||
| if (isDuplicateEvent(tx.transactionId, tx)) return; | ||
| setRecentTransactions(prev => [tx, ...prev.slice(0, 49)]); | ||
| onTransaction?.(tx); | ||
| }); | ||
|
|
||
| socket.on('metrics:update', (sysMetrics: SystemMetricsEvent) => { | ||
| setMetrics(sysMetrics); | ||
| }); | ||
| } | ||
|
|
||
| socketsRef.current.set(ns, socket); | ||
| }); | ||
| }, [url, token, maxReconnectAttempts, reconnectInterval, onConnectionChange, onError, onAlert, onScoreUpdate, onTransaction, isDuplicateEvent, flushQueue]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
connect identity churn can cause disconnect/reconnect loops.
connect depends on onConnectionChange, onError, onAlert, onScoreUpdate, onTransaction (Line 193). If a consumer passes inline callbacks (not memoized), connect is a new function each render, so the auto-connect effect at Lines 250-260 re-runs its cleanup (disconnect()) then connect() on every parent render, tearing down and rebuilding all sockets. Consider storing the callbacks in refs so connect stays stable, or narrow the effect dependencies.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/WebSocketProvider.tsx` around lines 113 - 193,
Prevent connect’s identity from changing when consumers provide inline callbacks
by storing onConnectionChange, onError, onAlert, onScoreUpdate, and
onTransaction in refs and invoking the current ref values inside the socket
handlers. Update the connect dependency list to use those stable refs while
preserving callback behavior, so the auto-connect effect does not repeatedly
disconnect and recreate sockets on parent renders.
| socket.on('connect', () => { | ||
| setStatus('connected'); | ||
| setLastConnected(new Date()); | ||
| setLastError(undefined); | ||
| setReconnectAttempts(0); | ||
| onConnectionChange?.('connected'); | ||
| flushQueue(); | ||
| }); | ||
|
|
||
| if (monitorRef.current) { | ||
| await monitorRef.current.stop(); | ||
| monitorRef.current = null; | ||
| socket.on('connect_error', (err: Error) => { | ||
| setLastError(err.message); | ||
| setStatus('error'); | ||
| onError?.(err); | ||
| }); | ||
|
|
||
| socket.on('reconnect_attempt', (attempt: number) => { | ||
| setStatus('reconnecting'); | ||
| setReconnectAttempts(attempt); | ||
| onConnectionChange?.('reconnecting'); | ||
| }); | ||
|
|
||
| socket.on('disconnect', (reason: string) => { | ||
| if (reason === 'io server disconnect') { | ||
| // Severed by server, try manual reconnect | ||
| socket.connect(); | ||
| } | ||
| setStatus('disconnected'); | ||
| onConnectionChange?.('disconnected'); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Aggregate connection state is incorrect for multi-namespace sockets.
A single status (plus reconnectAttempts/lastError) is shared across three independent sockets. Each socket's connect/disconnect/connect_error/reconnect_attempt handler overwrites the global state, so whichever socket fires last wins. A single namespace disconnecting flips status to 'disconnected' and fires onConnectionChange('disconnected') even while /alerts and /scores remain connected, and setReconnectAttempts(0) on any one connect masks ongoing reconnects on the others. Track state per namespace and derive an aggregate (e.g., connected only when all/required namespaces are connected).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/WebSocketProvider.tsx` around lines 132 - 160, Update
the WebSocketProvider connection handlers to track status, reconnect attempts,
and errors independently for each namespace/socket rather than overwriting
shared state. Derive the exposed aggregate status and onConnectionChange
notifications from all required namespaces, so one namespace disconnecting or
reconnecting does not report the whole connection as disconnected while others
remain connected. Keep connect events scoped to their namespace and only reset
aggregate reconnect state when no namespace is still reconnecting.
| const subscribe = useCallback(<T = any>( | ||
| namespace: WebSocketNamespace | string, | ||
| eventName: string, | ||
| callback: (data: T) => void | ||
| ): (() => void) => { | ||
| const socket = socketsRef.current.get(namespace); | ||
| if (socket) { | ||
| const wrappedHandler = (data: T) => { | ||
| if (!isDuplicateEvent(undefined, data)) { | ||
| callback(data); | ||
| } | ||
| }; | ||
| socket.on(eventName, wrappedHandler); | ||
| return () => { | ||
| socket.off(eventName, wrappedHandler); | ||
| }; | ||
| } | ||
| return () => {}; | ||
| }, [isDuplicateEvent]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
subscribe attaches to a socket captured at call time and never re-attaches.
subscribe reads socketsRef.current.get(namespace) once. Two problems make consumer subscriptions unreliable:
- Child effects run before the parent's auto-connect effect, so on mount the sockets map is usually empty and
subscribereturns the no-op() => {}(Line 246). Becausesubscribeis stable (deps[isDuplicateEvent]),useWebSocketEvent's effect never re-runs to retry once sockets exist. - When
connect()recreates sockets (reconnect/prop change), existing subscriptions still point at the old, disconnected socket instance.
Consider maintaining a registry of subscriptions in a ref and (re)binding them whenever a namespace socket is created, so handlers survive socket recreation and late connects.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/WebSocketProvider.tsx` around lines 229 - 247, Update
subscribe and the socket lifecycle to maintain a ref-backed registry of
namespace/event/callback subscriptions instead of binding only to the socket
returned at call time. Ensure subscriptions registered before a socket exists
are bound when that namespace socket is created, and existing subscriptions are
detached from replaced sockets and reattached to the new instances during
connect/reconnect, while preserving duplicate-event filtering and unsubscribe
behavior.
| console.log(`Connected ${CONCURRENT_CLIENT_COUNT} clients concurrently in ${connectionDuration}ms`); | ||
|
|
||
| // 2. Measure Broadcast Message Delivery Latency (< 100ms requirement) | ||
| const alertClients = connectedSockets.filter(s => s.nsp.endsWith('/alerts')); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the shipped type definitions for socket.io-client's Socket class
fd -HI 'socket.d.ts' node_modules/socket.io-client 2>/dev/null
rg -n 'nsp' node_modules/socket.io-client/build/**/socket.d.ts 2>/dev/nullRepository: nexoraorg/chenaikit
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## package.json dependency hints\n'
rg -n '"socket\.io-client"|socket\.io-client' package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
printf '\n## relevant test context\n'
sed -n '1,140p' tests/integration/websocket.integration.test.tsRepository: nexoraorg/chenaikit
Length of output: 4617
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## search for socket.io client API usage in repo\n'
rg -n '\.nsp\b|socket\.nsp|namespace' tests src . 2>/dev/null | sed -n '1,120p'Repository: nexoraorg/chenaikit
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json, os, re
from pathlib import Path
for path in ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"]:
p = Path(path)
if p.exists():
print(f"## {path}")
if path == "package.json":
data = json.loads(p.read_text())
deps = {}
deps.update(data.get("dependencies", {}))
deps.update(data.get("devDependencies", {}))
for k,v in deps.items():
if "socket.io-client" in k or "socket.io" in k:
print(k, v)
else:
text = p.read_text(errors="ignore")
m = re.search(r'socket\.io-client[^\n]*', text)
if m:
print(m.group(0)[:300])
PYRepository: nexoraorg/chenaikit
Length of output: 209
🌐 Web query:
socket.io-client v4 Socket nsp public property docs TypeScript definition
💡 Result:
In socket.io-client v4, the nsp property on the Socket class is a private property of type string [1][2][3]. It represents the namespace to which the socket instance belongs [4][5]. Because it is marked as private within the Socket class definition [1][2][3], it is not intended for public access in TypeScript and will not be accessible via type-checked code on a Socket instance. On the server-side version of Socket.IO, socket.nsp refers to the Namespace object [6][7], which is a public-facing property that allows interaction with the namespace (e.g., broadcasting events). However, this distinction is important because the client-side Socket instance does not expose this same Namespace object as a public property [8][1][2]. The client-side Socket's nsp property is used internally to track the socket's namespace assignment [1][2].
Citations:
- 1: https://github.com/socketio/socket.io-client/blob/master/lib/socket.ts
- 2: https://github.com/socketio/socket.io-client/blob/4.7.5/lib/socket.ts
- 3: https://cdn.jsdelivr.net/npm/socket.io-client@4.0.1/build/socket.d.ts
- 4: https://socket.io/docs/v3/client-api/
- 5: https://socket.io/docs/v2/client-api/
- 6: https://socket.io/docs/v4/namespaces/
- 7: https://socket.io/docs/v3/namespaces/
- 8: https://socket.io/docs/v4/client-socket-instance/
🌐 Web query:
socket.io-client 4.8.3 Socket nsp private TypeScript declaration
💡 Result:
In socket.io-client version 4.8.3, the nsp property on the Socket class is marked as private in the TypeScript declarations [1][2][3]. Consequently, TypeScript will flag an error if you attempt to access it directly (e.g., socket.nsp) [4]. This property is intended for internal use by the library to manage namespace routing for packets [2]. While it is public in the server-side Socket.IO implementation (allowing access to the Namespace instance) [4][5], the client-side Socket class uses it as a private string to identify which namespace the specific socket instance is connected to [2][3]. If you need to identify the namespace a client-side socket is connected to, consider storing the namespace string explicitly when you initialize the connection: const namespace = "/my-namespace"; const socket = io(namespace); // Instead of accessing socket.nsp, use your stored variable console.log(namespace); If you absolutely must access this private property in a TypeScript environment, you can bypass the type check by casting the socket instance to any: const nsp = (socket as any).nsp; However, this is not recommended as it relies on internal, undocumented behavior that is subject to change without notice.
Citations:
- 1: https://cdn.jsdelivr.net/npm/node-event-test-package@1.1.66/sample/subscriber/node_modules/socket.io-client/build/esm/socket.d.ts
- 2: https://github.com/socketio/socket.io-client/blob/master/lib/socket.ts
- 3: https://github.com/socketio/socket.io-client/blob/4.7.5/lib/socket.ts
- 4: Cannot access private nsp property on sockets socketio/socket.io-deno#16
- 5: https://socket.io/docs/v4/namespaces/
🌐 Web query:
site:socket.io/docs/v4/client-socket-instance nsp socket.io-client
💡 Result:
In the context of the socket.io-client library, nsp generally refers to a namespace. Namespaces allow you to split the application logic over a single shared connection [1]. While the provided documentation highlights the Socket instance properties such as socket.io (a reference to the Manager), it does not explicitly document an nsp property on the client-side Socket instance [1]. To connect to a specific namespace from the client, you define the namespace in the connection URL string [1]. Example of connecting to a specific namespace: const socket = io("http://localhost:3000/my-namespace"); In this example, /my-namespace is the namespace (nsp) being targeted [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## tsconfig files\n'
git ls-files '*tsconfig*.json' '*jest*.js' '*jest*.ts' 'package.json' | sed -n '1,80p'
printf '\n## package.json scripts and test tooling\n'
python3 - <<'PY'
import json
from pathlib import Path
p = Path('package.json')
if p.exists():
data = json.loads(p.read_text())
for k,v in data.get('scripts', {}).items():
if 'test' in k.lower() or 'type' in k.lower() or 'lint' in k.lower():
print(f'{k}: {v}')
for sec in ('devDependencies','dependencies'):
for name in ('typescript','ts-jest','jest','vitest','babel-jest'):
if name in data.get(sec, {}):
print(f'{sec}:{name}={data[sec][name]}')
PY
printf '\n## relevant tsconfig excerpt\n'
for f in $(git ls-files '*tsconfig*.json'); do
echo "--- $f"
sed -n '1,220p' "$f" | sed -n '1,160p'
doneRepository: nexoraorg/chenaikit
Length of output: 6391
🌐 Web query:
socket.io-client 4.8.3 lib/socket.ts nsp private Socket class
💡 Result:
In socket.io-client version 4.8.3, the Socket class defines the nsp property as a private member [1][2]. Within the lib/socket.ts source file, it is declared as: private readonly nsp: string; This property is intended for internal use by the Socket instance to track which namespace it belongs to and is not exposed as a public API for external access [1][3]. While some other implementations of Socket.IO (such as the Deno port) have historically encountered issues where this property was incorrectly marked as private when external access was expected [4], in the official socket.io-client package, the namespace is managed internally by the class and generally does not require direct access from the application layer [5][6].
Citations:
- 1: https://github.com/socketio/socket.io-client/blob/master/lib/socket.ts
- 2: https://fossies.org/linux/socket.io/packages/socket.io-client/lib/socket.ts
- 3: https://github.com/socketio/socket.io-client/blob/4.7.5/lib/socket.ts
- 4: Cannot access private nsp property on sockets socketio/socket.io-deno#16
- 5: https://socket.io/docs/v4/client-socket-instance/
- 6: https://socket.io/docs/v4/client-api/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
p = Path('package.json')
if p.exists():
data = json.loads(p.read_text())
print(data.get('scripts', {}).get('test', ''))
print(data.get('scripts', {}).get('typecheck', ''))
print(data.get('scripts', {}).get('lint', ''))
PY
for f in $(git ls-files '*tsconfig*.json'); do
echo "--- $f"
rg -n '"strict"|noEmitOnError|skipLibCheck|types|include|exclude' "$f" || true
doneRepository: nexoraorg/chenaikit
Length of output: 1351
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## tests/integration/package.json\n'
if [ -f tests/integration/package.json ]; then
cat -n tests/integration/package.json
else
echo 'missing'
fi
printf '\n## root package.json scripts mentioning type-check/test:integration\n'
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path('package.json').read_text())
for k,v in data.get('scripts', {}).items():
if 'type-check' in k.lower() or 'integration' in k.lower():
print(f'{k}: {v}')
PY
printf '\n## tests/integration jest config\n'
cat -n tests/integration/jest.config.jsRepository: nexoraorg/chenaikit
Length of output: 1517
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## root workspace package manifests\n'
for f in $(git ls-files '*/package.json' 'package.json'); do
case "$f" in
tests/integration/package.json|package.json)
echo "--- $f"
sed -n '1,220p' "$f"
;;
esac
doneRepository: nexoraorg/chenaikit
Length of output: 4379
Avoid relying on socket.nsp here. socket.io-client v4 treats nsp as a private client field, so filter on the namespace string you already have when creating each socket instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/integration/websocket.integration.test.ts` at line 83, Update the alert
client filtering around connectedSockets to avoid the private socket.nsp field.
Preserve each socket’s namespace string when creating the clients and filter
using that stored namespace, selecting entries whose namespace ends with
“/alerts”.
|
@onajidavid87-web resolve conflict |
Implemented a scalable real-time WebSocket event system using Express and Socket.IO with JWT authentication, Redis Pub/Sub horizontal scaling, and namespace-based routing (/alerts, /scores, /transactions). Included a resilient React WebSocketProvider with offline message queuing, custom hooks (useWebSocket, useWebSocketEvent), event deduplication, and integration stress testing for 100+ concurrent connections under 100ms latency.
Closes #259
Summary by CodeRabbit
New Features
Documentation
Tests