Enhance call handling with state management and WebRTC integration#26
Enhance call handling with state management and WebRTC integration#26harshlocham wants to merge 122 commits into
Conversation
…ns and media streams
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Introduces foundational call signaling + WebRTC scaffolding to support audio/video calling in the chat app, including shared socket payload/types, a backend call signaling handler, and frontend state/hooks for signaling + peer connection management.
Changes:
- Added shared socket event names and payload types for call lifecycle + signaling (offer/answer/ICE/accept/reject/end/state).
- Registered a new socket
callHandlerand added Redis key/TTL primitives for call lifecycle tracking. - Implemented frontend call state (Zustand) plus hooks for call signaling and WebRTC peer connection management.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/types/socket/payloads.ts | Adds call-related payload/type definitions shared across client/server. |
| packages/types/socket/events.ts | Adds call socket event constants + typed event maps. |
| packages/services/call.service.ts | Adds call state machine helpers (transitions/terminal states). |
| apps/web/store/call-store.ts | Introduces Zustand call store + selectors for UI state. |
| apps/web/lib/webrtc/peer-manager.ts | Adds a wrapper around RTCPeerConnection lifecycle and signaling helpers. |
| apps/web/hooks/useWebRTC.ts | Adds React hook integrating PeerManager + local/remote stream handling. |
| apps/web/hooks/useCallState.ts | Adds convenience hook to access call store state/selectors. |
| apps/web/hooks/useCallSignaling.ts | Adds hook to listen/emit call signaling socket events and update store. |
| apps/socket/server/socket/keys.ts | Adds Redis key patterns + TTL constants for call state primitives. |
| apps/socket/server/socket/index.ts | Registers callHandler on socket connection. |
| apps/socket/server/socket/handlers/call/call.handler.ts | Implements server-side call signaling relays using user-specific rooms. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| socket.on(SocketEvents.CALL_OFFER, ({ to, offer }: CallOfferPayload) => { | ||
| io.to(to).emit(SocketEvents.CALL_OFFER, { | ||
| from: socket.data.userId, | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_OFFER, { | ||
| from, | ||
| to, | ||
| offer, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
CALL_OFFER handler forwards only {from,to,offer} and drops call-scoping metadata already defined in CallOfferPayload (e.g., callId, conversationId, sdpRevision, deviceId). Without forwarding those fields, clients can’t reliably correlate offers when multiple calls/signaling sessions exist.
| socket.on(SocketEvents.CALL_ANSWER, ({ to, answer }: CallAnswerPayload) => { | ||
| io.to(to).emit(SocketEvents.CALL_ANSWER, { | ||
| from: socket.data.userId, | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_ANSWER, { | ||
| from, | ||
| to, | ||
| answer, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
CALL_ANSWER handler forwards only {from,to,answer} and drops CallAnswerPayload fields like callId/sdpRevision/deviceId. This makes the answer ambiguous in concurrent/overlapping calls and loses versioning hints; forward the relevant fields from the inbound payload.
| socket.on(SocketEvents.CALL_ICE_CANDIDATE, ({ to, candidate, callId }: CallIceCandidatePayload) => { | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, { | ||
| callId, | ||
| from, | ||
| to, | ||
| candidate, | ||
| }); | ||
| }); | ||
|
|
There was a problem hiding this comment.
CALL_ICE_CANDIDATE relay drops sequencing/indexing fields from CallIceCandidatePayload (candidateSeq, mid, mLineIndex). If clients rely on ordering or specific m-line targeting, this data loss can break ICE replay/reconnect flows; forward these optional fields when present.
| socket.on(SocketEvents.CALL_ICE_CANDIDATE, ({ to, candidate, callId }: CallIceCandidatePayload) => { | |
| const from = socket.data.userId; | |
| if (!to) return; | |
| io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, { | |
| callId, | |
| from, | |
| to, | |
| candidate, | |
| }); | |
| }); | |
| socket.on( | |
| SocketEvents.CALL_ICE_CANDIDATE, | |
| ({ | |
| to, | |
| candidate, | |
| callId, | |
| candidateSeq, | |
| mid, | |
| mLineIndex, | |
| }: CallIceCandidatePayload) => { | |
| const from = socket.data.userId; | |
| if (!to) return; | |
| io.to(userRoom(to)).emit(SocketEvents.CALL_ICE_CANDIDATE, { | |
| callId, | |
| from, | |
| to, | |
| candidate, | |
| candidateSeq, | |
| mid, | |
| mLineIndex, | |
| }); | |
| } | |
| ); |
| socket.on(SocketEvents.CALL_END, ({ to }: CallEndPayload) => { | ||
| io.to(to).emit(SocketEvents.CALL_END, { | ||
| from: socket.data.userId, | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_END, { | ||
| from, | ||
| to, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
CALL_END relay ignores optional CallEndPayload fields (callId, reason, endedAt) and emits only {from,to}. This prevents the receiver from validating which call ended and why; include the callId and propagate reason/timestamp when provided.
| socket.on(SocketEvents.CALL_BUSY, ({ to }: CallRingingPayload) => { | ||
| io.to(to).emit(SocketEvents.CALL_BUSY, { | ||
| from: socket.data.userId, | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_BUSY, { | ||
| from, | ||
| to, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
CALL_BUSY relay emits only {from,to} even though callId/conversationId can be present on CallRingingPayload. For correctness with overlapping calls, include callId (and conversationId if available) so the callee can associate the busy signal with the right call.
| socket.on(SocketEvents.CALL_RECONNECT, ({ to, callId }) => { | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_RECONNECT, { | ||
| callId, | ||
| from, | ||
| to, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
CALL_RECONNECT relay drops CallReconnectPayload fields like lastSeenSeq and iceRestartRequired. Those are typically needed to decide whether to resend buffered candidates/SDP or trigger an ICE restart; forward them (and validate callId) to support reconnection semantics.
| socket.on( | ||
| SocketEvents.CALL_OFFER_INIT, | ||
| ({ callId, conversationId, to, callType }: CallOfferInitPayload) => { | ||
| const from = socket.data.userId; | ||
| if (!to) return; | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_RINGING, { | ||
| callId, | ||
| conversationId, | ||
| from, | ||
| to, | ||
| }); | ||
|
|
||
| const statePayload: CallStatePayload = { | ||
| callId, | ||
| conversationId, | ||
| status: "ringing", | ||
| participants: [{ userId: from }, { userId: to }], | ||
| serverTs: new Date(), | ||
| }; | ||
|
|
||
| io.to(userRoom(from)).emit(SocketEvents.CALL_STATE, statePayload); | ||
| io.to(userRoom(to)).emit(SocketEvents.CALL_STATE, statePayload); | ||
|
|
||
| io.to(userRoom(to)).emit(SocketEvents.CALL_OFFER_INIT, { | ||
| callId, | ||
| conversationId, | ||
| from, | ||
| to, | ||
| callType, | ||
| }); | ||
| } |
There was a problem hiding this comment.
CALL_OFFER_INIT currently emits both CALL_RINGING and CALL_OFFER_INIT to the callee with overlapping data, plus CALL_STATE to both sides. This duplication increases the chance of double-notifications and diverging client behavior; consider consolidating to a single “incoming call” event (or clearly separating responsibilities/payloads).
| useEffect(() => { | ||
| const onRinging = (payload: CallOfferInitPayload) => { | ||
| setIncomingCall({ | ||
| callId: payload.callId, | ||
| conversationId: payload.conversationId, | ||
| callType: payload.callType, | ||
| participants: [{ userId: payload.from }, { userId: payload.to }], | ||
| }); | ||
| }; |
There was a problem hiding this comment.
The incoming-call handler is named onRinging but is wired to SocketEvents.CALL_OFFER_INIT (not CALL_RINGING). Given both events exist, this is easy to misread/maintain; either subscribe to CALL_RINGING (and use CallRingingPayload) or rename the handler/event usage to match intent.
| const onCallAccepted = () => { | ||
| stopRingtone(); | ||
| setStatus("accepted"); | ||
| }; | ||
|
|
||
| const onCallRejected = () => { | ||
| stopRingtone(); | ||
| setStatus("rejected"); | ||
| }; | ||
|
|
||
| const onCallEnded = () => { | ||
| endCall(); | ||
| }; |
There was a problem hiding this comment.
CALL_ACCEPT / CALL_REJECT / CALL_END handlers update global call state without checking which call the event belongs to (payload includes callId). If a user can receive stale/overlapping events, this can incorrectly flip the UI state; gate the updates by comparing payload.callId to the current store callId.
| export enum CallState { | ||
| INITIATED = "initiated", | ||
| RINGING = "ringing", | ||
| ACCEPTED = "accepted", | ||
| ACTIVE = "active", | ||
| RECONNECTING = "reconnecting", | ||
| ENDED = "ended", | ||
| REJECTED = "rejected", | ||
| MISSED = "missed", | ||
| FAILED = "failed", | ||
| } |
There was a problem hiding this comment.
packages/types defines a CallState string union, while packages/services exports a CallState enum with the same exported name but similar values. This naming collision makes cross-package imports ambiguous and error-prone; consider renaming the enum (e.g., CallLifecycleState) or re-exporting/deriving it from the shared @chat/types CallState.
…and session management
…es; extend tsconfig from expo
…d integrating API validation
…son and package-lock.json
…horization header methods
…services packages
…rtifact management
…ng across shared packages
- Added new `RELEASES.md` file to document the release architecture and CI/CD workflow responsibilities. - Updated `.changeset/config.json` to ignore specific packages during versioning. - Enhanced `deploy.yml` to support manual promotion of releases with new inputs for `release_tag` and `deploy_target`. - Modified `release.yml` to compute and output the short SHA of the release commit for better tracking.
- Updated `deploy.yml` to support manual promotion of releases with new inputs for `release_tag` and `deploy_target`. - Improved deployment logic to handle both automatic and manual triggers, ensuring proper metadata resolution. - Enhanced `RELEASES.md` to document the new manual promotion process and health check configurations. - Added rollback triggers and notification features for production deployments.
- Removed debug logging from the socket handshake authentication process. - Enhanced the `getClientSocketUrl` function to provide a default local socket URL and handle various local development scenarios. - Updated the chat store to use a consistent `idOf` function for conversation identification, improving code clarity and maintainability. - Added unit tests for the `getClientSocketUrl` function to ensure correct behavior in different environments.
…pts in package.json for direct execution
…d streamline image domain configuration
- Added Prettier and related plugins to improve code formatting. - Updated Next.js version in package.json and adjusted build commands in vercel.json for consistency. - Refactored scripts in web's package.json for direct execution of Next.js commands. - Cleaned up next.config.ts by removing unnecessary environment loading logic.
…age.json and package-lock.json
This pull request introduces the foundational backend and frontend logic for real-time call signaling and WebRTC integration, enabling audio/video call features in the chat application. The changes include new socket event handlers for call signaling on the server, a client-side Zustand call state store, custom React hooks for managing call state and signaling, and a WebRTC peer connection manager.
The most important changes are:
Backend: Call Signaling and State Management
call.handler.tsto support call initiation, offer/answer exchange, ICE candidate relay, call acceptance/rejection, ending, and reconnection, using user-specific rooms for targeted signaling. [1] [2]keys.tsfor tracking call states and participants, laying groundwork for call lifecycle management and concurrency control.Backend Integration
callHandlerin the socket server initialization to activate call signaling features. [1] [2]Frontend: Call State Management
call-store.tsto manage the local call state, including participants, call status, direction, media states, and error handling, with convenient selectors for UI logic.Frontend: Call Signaling and WebRTC
useCallSignalingReact hook for handling socket events and emitting signaling messages, integrating with the call store for state updates and error handling.useCallStatehook for easy access to call state and selectors from components.PeerManagerclass anduseWebRTChook to encapsulate WebRTC peer connection logic, including ICE candidate handling, offer/answer flow, media stream management, and connection state changes. [1] [2]