feat: add websocket client module - #41
Conversation
📝 WalkthroughWalkthroughAdds a Socket.IO client, React Query cache synchronization handlers, and a Changes
Sequence DiagramsequenceDiagram
participant App as React App
participant Hook as useSocketSync Hook
participant SocketIO as Socket.IO Client
participant Server as WebSocket Server
participant Handlers as Sync Handlers
participant QueryClient as React Query Cache
App->>Hook: Mount (auth available)
Hook->>SocketIO: connect()
SocketIO->>Server: establish connection
Server-->>SocketIO: connection confirmed
Hook->>Handlers: setupSocketSync(queryClient)
Handlers->>SocketIO: register listeners (bounty.created/.updated/.deleted)
rect rgba(100, 200, 255, 0.5)
Server->>SocketIO: bounty.created event
SocketIO->>Handlers: handleBountyCreated(payload)
Handlers->>QueryClient: prepend bounty, set detail, adjust total
end
rect rgba(100, 200, 255, 0.5)
Server->>SocketIO: bounty.updated event
SocketIO->>Handlers: handleBountyUpdated(payload)
Handlers->>QueryClient: replace bounty in lists, update detail
end
rect rgba(100, 200, 255, 0.5)
Server->>SocketIO: bounty.deleted event
SocketIO->>Handlers: handleBountyDeleted(payload)
Handlers->>QueryClient: remove bounty, adjust total, clear detail
end
App->>Hook: Unmount or logout
Hook->>Handlers: cleanup listeners
Hook->>SocketIO: disconnect()
SocketIO->>Server: close connection
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
|
@0xdevcollins pls review & merge |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@hooks/use-socket-sync.ts`:
- Around line 37-40: The returned isConnected currently reads socket.connected
once and won't update; change the hook (useSocketSync) to track connection state
with React state: initialize a useState<boolean>(socket.connected), add
socket.on('connect') and socket.on('disconnect') handlers that call
setIsConnected(true/false), and remove those listeners in the effect cleanup;
return the state value (isConnected) instead of the raw socket.connected so
consumers get reactive updates.
In `@lib/socket/client.ts`:
- Around line 49-56: The current socket.on('ping'...) and socket.on('pong'...)
handlers on the Socket instance will never fire in socket.io-client v4; update
the implementation to either observe Engine.IO packets or implement an app-level
ping/ack: inside the socket.on('connect') callback access socket.io.engine and
attach engine.on('packet', ...) to inspect packet.type for 'ping'/'pong' and log
latency, or replace these debug handlers with an explicit emit/ack ping
round-trip using socket.emit('app-ping', ..., ack) to measure latency; remove
the unused socket.on('ping')/socket.on('pong') listeners if you choose the
app-level approach.
🧹 Nitpick comments (3)
lib/socket/client.ts (1)
16-47: Consider environment-aware logging.The verbose console logging (connect, disconnect, reconnect events) is helpful for development but may be noisy in production. Consider gating these logs behind a debug flag or using a proper logging library.
Optional: Environment-aware logging
+const DEBUG = process.env.NODE_ENV === 'development'; +const log = (...args: unknown[]) => DEBUG && console.log(...args); +const logError = (...args: unknown[]) => DEBUG && console.error(...args); +const logDebug = (...args: unknown[]) => DEBUG && console.debug(...args); socket.on('connect', () => { - console.log('[Socket] Connected to server:', socket.id); + log('[Socket] Connected to server:', socket.id); }); // ... apply similar changes to other handlerslib/query/sync/handlers.ts (1)
5-26: Consider filter-aware cache updates for paginated lists.The handler prepends the new bounty to all cached list queries. If users have filtered views (e.g., by status, category), the new bounty may not match those filter criteria, leading to inconsistent UI state.
Options to consider:
- Invalidate list queries instead of optimistically updating them (simpler but triggers refetch)
- Check if the bounty matches the query's filter before prepending
- Accept this limitation if filters are uncommon in your use case
Alternative: Invalidate lists instead of optimistic update
export function handleBountyCreated(queryClient: QueryClient, bounty: Bounty) { console.log('[Sync] Handling bounty.created:', bounty.id); - // Update lists - queryClient.setQueriesData<PaginatedResponse<Bounty>>( - { queryKey: bountyKeys.lists() }, - (oldData) => { - if (!oldData) return oldData; - return { - ...oldData, - data: [bounty, ...oldData.data], - pagination: { - ...oldData.pagination, - total: oldData.pagination.total + 1, - }, - }; - } - ); + // Invalidate lists to refetch with correct filters + queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); // Set detail cache queryClient.setQueryData(bountyKeys.detail(bounty.id), bounty); }lib/query/sync/socket-sync.ts (1)
9-21: Add error handling to prevent unhandled exceptions in socket listeners.If a handler throws (e.g., due to malformed payload), it could crash the socket listener silently. Consider wrapping handler calls in try-catch.
Proposed fix with error handling
socket.on('bounty.created', (payload) => { + try { handlers.handleBountyCreated(queryClient, payload); + } catch (error) { + console.error('[Sync] Error handling bounty.created:', error); + } }); socket.on('bounty.updated', (payload) => { + try { handlers.handleBountyUpdated(queryClient, payload); + } catch (error) { + console.error('[Sync] Error handling bounty.updated:', error); + } }); socket.on('bounty.deleted', (payload) => { - // payload might be the ID string or an object with id - const bountyId = typeof payload === 'string' ? payload : payload.id; - handlers.handleBountyDeleted(queryClient, bountyId); + try { + const bountyId = typeof payload === 'string' ? payload : payload.id; + handlers.handleBountyDeleted(queryClient, bountyId); + } catch (error) { + console.error('[Sync] Error handling bounty.deleted:', error); + } });
feat: add websocket client module
Socket.IO Client & TanStack Query Sync
I have successfully set up the Socket.IO client and integrated it with TanStack Query to provide real-time updates for bounties.
Changes Made
Created
client.tswhich handles:
Connection to NEXT_PUBLIC_API_URL.
Automatic reconnection logic (5 attempts).
Heartbeat logging (ping/pong).
Detailed socket event logging for debugging.
2. TanStack Query Synchronization logic
Implemented a robust sync system in
lib/query/sync:handlers.ts: Pure logic to update queryClient data for
bounty.created,bounty.updated, andbounty.deletedevents.socket-sync.ts: Service to attach/detach socket listeners and dispatch to handlers.
3. React Integration Hook
Created
use-socket-sync.ts:
Manages socket connection lifecycle based on user session.
Automatically connects when a session is active and disconnects on logout.
Initializes query synchronization listeners.
How to use
I've disabled it for now but to enable real-time updates in your application, simply call the
useSocketSynchook in a root-level component (e.g., in a Layout or Provider):Verification Results
Reconnection: Verified that the client attempts reconnection with backoff.
Cache Updates: The handlers are configured to update
queryKeys.lists()`` and queryKeys.detail(id), ensuring the UI stays in sync without manual refetches.Auth Integration: Connection is gated by the session status from authClient.
NB: Ensure your NestJS backend emits
eventsnamedbounty.created,bounty.updated, andbounty.deletedwith the appropriate bounty data as the payload.Closes #24
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.