From fb40d252344a3d928c6d209df2b37f68c93864ee Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Jun 2025 07:52:51 -0700 Subject: [PATCH] fix user presence --- .../w/[workflowId]/hooks/use-presence.ts | 25 +++++++-- apps/sim/contexts/socket-context.tsx | 55 ++++++++++++++++--- apps/sim/socket-server/handlers/workflow.ts | 3 +- apps/sim/socket-server/rooms/manager.ts | 16 ++++++ 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts index 2e3b1a5f8a..23afeebc4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts @@ -3,6 +3,16 @@ import { useMemo } from 'react' import { useSocket } from '@/contexts/socket-context' +// Socket presence user from server +interface SocketPresenceUser { + socketId: string + userId: string + userName: string + cursor?: { x: number; y: number } + selection?: { type: 'block' | 'edge' | 'none'; id?: string } +} + +// UI presence user for components type PresenceUser = { connectionId: string | number name?: string @@ -24,10 +34,17 @@ export function usePresence(): UsePresenceReturn { const { presenceUsers, isConnected } = useSocket() const users = useMemo(() => { - return presenceUsers.map((user, index) => ({ - // Use socketId directly as connectionId to ensure uniqueness - // If no socketId, use a unique fallback based on userId + index - connectionId: user.socketId || `fallback-${user.userId}-${index}`, + // Deduplicate by userId - only show one presence per unique user + const uniqueUsers = new Map() + + presenceUsers.forEach((user) => { + // Keep the most recent presence for each user (last one wins) + uniqueUsers.set(user.userId, user) + }) + + return Array.from(uniqueUsers.values()).map((user) => ({ + // Use userId as connectionId since we've deduplicated + connectionId: user.userId, name: user.userName, color: undefined, // Let the avatar component generate colors info: user.selection?.type ? `Editing ${user.selection.type}` : undefined, diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx index 2453e25f80..45d612ebbe 100644 --- a/apps/sim/contexts/socket-context.tsx +++ b/apps/sim/contexts/socket-context.tsx @@ -101,7 +101,21 @@ export function SocketProvider({ children, user }: SocketProviderProps) { // Initialize socket when user is available useEffect(() => { - if (!user?.id || socket) return + if (!user?.id) return + + // Prevent duplicate connections - disconnect existing socket first + if (socket) { + logger.info('Disconnecting existing socket before creating new one') + socket.disconnect() + setSocket(null) + setIsConnected(false) + } + + // Prevent multiple simultaneous initialization attempts + if (isConnecting) { + logger.info('Socket initialization already in progress, skipping') + return + } logger.info('Initializing socket connection for user:', user.id) setIsConnecting(true) @@ -282,8 +296,16 @@ export function SocketProvider({ children, user }: SocketProviderProps) { // Start the socket initialization initializeSocket() - // Cleanup on unmount + // Cleanup on unmount or user change return () => { + if (socket) { + logger.info('Cleaning up socket connection') + socket.disconnect() + setSocket(null) + setIsConnected(false) + setIsConnecting(false) + } + positionUpdateTimeouts.current.forEach((timeoutId) => { clearTimeout(timeoutId) }) @@ -295,15 +317,30 @@ export function SocketProvider({ children, user }: SocketProviderProps) { // Join workflow room const joinWorkflow = useCallback( (workflowId: string) => { - if (socket && user?.id) { - logger.info(`Joining workflow: ${workflowId}`) - socket.emit('join-workflow', { - workflowId, // Server gets user info from authenticated session - }) - setCurrentWorkflowId(workflowId) + if (!socket || !user?.id) { + logger.warn('Cannot join workflow: socket or user not available') + return } + + // Prevent duplicate joins to the same workflow + if (currentWorkflowId === workflowId) { + logger.info(`Already in workflow ${workflowId}, skipping join`) + return + } + + // Leave current workflow first if we're in one + if (currentWorkflowId) { + logger.info(`Leaving current workflow ${currentWorkflowId} before joining ${workflowId}`) + socket.emit('leave-workflow') + } + + logger.info(`Joining workflow: ${workflowId}`) + socket.emit('join-workflow', { + workflowId, // Server gets user info from authenticated session + }) + setCurrentWorkflowId(workflowId) }, - [socket, user] + [socket, user, currentWorkflowId] ) // Leave current workflow room diff --git a/apps/sim/socket-server/handlers/workflow.ts b/apps/sim/socket-server/handlers/workflow.ts index e21bbc8ad4..49733a6ada 100644 --- a/apps/sim/socket-server/handlers/workflow.ts +++ b/apps/sim/socket-server/handlers/workflow.ts @@ -97,8 +97,9 @@ export function setupWorkflowHandlers( // Broadcast updated presence list to all users in the room roomManager.broadcastPresenceUpdate(workflowId) + const uniqueUserCount = roomManager.getUniqueUserCount(workflowId) logger.info( - `User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${room.activeConnections} users.` + `User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${uniqueUserCount} unique users (${room.activeConnections} connections).` ) } catch (error) { logger.error('Error joining workflow:', error) diff --git a/apps/sim/socket-server/rooms/manager.ts b/apps/sim/socket-server/rooms/manager.ts index c9a5efc3c3..1875226a03 100644 --- a/apps/sim/socket-server/rooms/manager.ts +++ b/apps/sim/socket-server/rooms/manager.ts @@ -180,4 +180,20 @@ export class RoomManager { this.io.to(workflowId).emit('presence-update', roomPresence) } } + + /** + * Get the number of unique users in a workflow room + * (not the number of socket connections) + */ + getUniqueUserCount(workflowId: string): number { + const room = this.workflowRooms.get(workflowId) + if (!room) return 0 + + const uniqueUsers = new Set() + room.users.forEach((presence) => { + uniqueUsers.add(presence.userId) + }) + + return uniqueUsers.size + } }