Skip to content

Socket.IO Event Management

Comprehensive guide to managing Socket.IO events with Response Handler.

Event Handler Setup

Basic Event Structure

javascript
const { Server } = require('socket.io');
const { quickSocketSetup } = require('response-handler');

const io = new Server(server);

// Apply Response Handler to Socket.IO
io.use(
  quickSocketSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
  }),
);

io.on('connection', (socket) => {
  console.log(`Client connected: ${socket.id}`);

  // Event handlers
  socket.on('user:join', handleUserJoin);
  socket.on('message:send', handleMessageSend);
  socket.on('room:join', handleRoomJoin);
  socket.on('disconnect', handleDisconnect);
});

Connection Events

Connection Handling

javascript
function handleConnection(socket) {
  // Store connection info
  const connectionInfo = {
    socketId: socket.id,
    connectedAt: new Date(),
    userAgent: socket.handshake.headers['user-agent'],
    ip: socket.handshake.address,
    query: socket.handshake.query,
  };

  // Welcome message
  socket.ok(connectionInfo, 'Connected successfully');

  // Send initial data
  socket.emit('server:info', {
    serverTime: new Date().toISOString(),
    version: process.env.APP_VERSION || '1.0.0',
    features: ['chat', 'rooms', 'file-sharing'],
  });
}

function handleDisconnect(socket, reason) {
  console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);

  // Clean up user data
  const user = getUserBySocketId(socket.id);
  if (user) {
    removeUserFromAllRooms(user.id);
    notifyUserOffline(user.id);
  }

  // Log disconnect reason
  logDisconnection(socket.id, reason);
}

io.on('connection', (socket) => {
  handleConnection(socket);

  socket.on('disconnect', (reason) => {
    handleDisconnect(socket, reason);
  });
});

Connection Authentication

javascript
const jwt = require('jsonwebtoken');

// Authentication middleware for Socket.IO
const authenticateSocket = (socket, next) => {
  const token = socket.handshake.auth.token || socket.handshake.query.token;

  if (!token) {
    return next(new Error('Authentication token required'));
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) {
      return next(new Error('Invalid authentication token'));
    }

    socket.userId = decoded.id;
    socket.userRole = decoded.role;
    next();
  });
};

// Apply authentication middleware
io.use(authenticateSocket);

io.on('connection', (socket) => {
  socket.ok(
    {
      userId: socket.userId,
      role: socket.userRole,
    },
    'Authenticated successfully',
  );
});

User Events

User Registration and Login

javascript
// User join event
socket.on('user:join', async (data) => {
  try {
    const { username, avatar } = data;

    // Validation
    if (!username || username.trim().length < 2) {
      return socket.badRequest(
        { username: 'Username must be at least 2 characters' },
        'Invalid username',
      );
    }

    // Check if username is taken
    const existingUser = getUserByUsername(username);
    if (existingUser) {
      return socket.conflict({ username }, 'Username already taken');
    }

    // Create user session
    const user = {
      id: generateUserId(),
      socketId: socket.id,
      username: username.trim(),
      avatar: avatar || getDefaultAvatar(),
      joinedAt: new Date(),
      isOnline: true,
    };

    // Store user
    addUser(user);
    socket.user = user;

    // Join default room
    socket.join('general');

    // Notify user
    socket.ok(user, 'Joined successfully');

    // Notify others
    socket.to('general').emit('user:joined', {
      user: sanitizeUser(user),
      message: `${username} joined the chat`,
    });

    // Send current users list
    socket.emit('users:list', {
      users: getOnlineUsers().map(sanitizeUser),
      total: getOnlineUsers().length,
    });
  } catch (error) {
    socket.error(error, 'Failed to join');
  }
});

// User leave event
socket.on('user:leave', () => {
  try {
    const user = socket.user;
    if (!user) {
      return socket.badRequest({}, 'No active user session');
    }

    // Remove user
    removeUser(user.id);

    // Notify others
    socket.broadcast.emit('user:left', {
      user: sanitizeUser(user),
      message: `${user.username} left the chat`,
    });

    socket.ok({}, 'Left successfully');
  } catch (error) {
    socket.error(error, 'Failed to leave');
  }
});

User Status Management

javascript
// Update user status
socket.on('user:status', (data) => {
  try {
    const { status, message } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    const validStatuses = ['online', 'away', 'busy', 'invisible'];
    if (!validStatuses.includes(status)) {
      return socket.badRequest(
        {
          status,
          validStatuses,
        },
        'Invalid status',
      );
    }

    // Update user status
    user.status = status;
    user.statusMessage = message || null;
    user.lastActivity = new Date();

    updateUser(user);

    // Broadcast status change
    socket.broadcast.emit('user:status_changed', {
      userId: user.id,
      username: user.username,
      status,
      message,
    });

    socket.ok({ status, message }, 'Status updated');
  } catch (error) {
    socket.error(error, 'Failed to update status');
  }
});

// Get user profile
socket.on('user:profile', (data) => {
  try {
    const { userId } = data;

    if (!userId) {
      // Return own profile
      if (!socket.user) {
        return socket.unauthorized({}, 'User not authenticated');
      }

      return socket.ok(sanitizeUser(socket.user), 'Profile retrieved');
    }

    // Return other user's profile
    const user = getUserById(userId);
    if (!user) {
      return socket.notFound({ userId }, 'User not found');
    }

    socket.ok(sanitizeUser(user), 'Profile retrieved');
  } catch (error) {
    socket.error(error, 'Failed to get profile');
  }
});

Message Events

Message Sending

javascript
// Send message event
socket.on('message:send', async (data) => {
  try {
    const { roomId, content, type = 'text', replyTo } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    // Validation
    if (!roomId) {
      return socket.badRequest({ requiredFields: ['roomId'] }, 'Room ID is required');
    }

    if (!content || content.trim().length === 0) {
      return socket.badRequest({ content: 'Message cannot be empty' }, 'Invalid message content');
    }

    if (content.length > 1000) {
      return socket.badRequest(
        {
          maxLength: 1000,
          currentLength: content.length,
        },
        'Message too long',
      );
    }

    // Check if user is in room
    const isInRoom = socket.rooms.has(roomId);
    if (!isInRoom) {
      return socket.forbidden({ roomId }, 'You are not a member of this room');
    }

    // Rate limiting for messages
    const lastMessage = getLastMessageByUser(user.id);
    if (lastMessage && Date.now() - lastMessage.timestamp < 1000) {
      return socket.tooManyRequests(
        { cooldown: 1000 },
        'Please wait before sending another message',
      );
    }

    // Create message
    const message = {
      id: generateMessageId(),
      roomId,
      userId: user.id,
      username: user.username,
      content: content.trim(),
      type,
      replyTo,
      timestamp: new Date(),
      edited: false,
    };

    // Save message
    await saveMessage(message);

    // Update user activity
    updateUserActivity(user.id);

    // Send to room
    io.to(roomId).emit('message:received', message);

    socket.ok({ messageId: message.id }, 'Message sent');
  } catch (error) {
    socket.error(error, 'Failed to send message');
  }
});

// Edit message event
socket.on('message:edit', async (data) => {
  try {
    const { messageId, content } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    const message = await getMessage(messageId);
    if (!message) {
      return socket.notFound({ messageId }, 'Message not found');
    }

    // Check ownership
    if (message.userId !== user.id) {
      return socket.forbidden({ messageId }, 'Can only edit your own messages');
    }

    // Check if message is too old (5 minutes)
    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
    if (message.timestamp < fiveMinutesAgo) {
      return socket.forbidden(
        {
          messageId,
          ageLimit: '5 minutes',
        },
        'Message too old to edit',
      );
    }

    // Update message
    message.content = content.trim();
    message.edited = true;
    message.editedAt = new Date();

    await updateMessage(message);

    // Broadcast update
    io.to(message.roomId).emit('message:updated', message);

    socket.ok({ messageId }, 'Message updated');
  } catch (error) {
    socket.error(error, 'Failed to edit message');
  }
});

Message Reactions

javascript
// Add reaction to message
socket.on('message:react', async (data) => {
  try {
    const { messageId, emoji } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    const message = await getMessage(messageId);
    if (!message) {
      return socket.notFound({ messageId }, 'Message not found');
    }

    // Validate emoji
    const validEmojis = ['👍', '👎', '❤️', '😂', '😮', '😢', '😡'];
    if (!validEmojis.includes(emoji)) {
      return socket.badRequest(
        {
          emoji,
          validEmojis,
        },
        'Invalid emoji',
      );
    }

    // Add reaction
    const reaction = await addReaction(messageId, user.id, emoji);

    // Broadcast reaction
    io.to(message.roomId).emit('message:reaction_added', {
      messageId,
      userId: user.id,
      username: user.username,
      emoji,
      reactionId: reaction.id,
    });

    socket.ok({ reactionId: reaction.id }, 'Reaction added');
  } catch (error) {
    socket.error(error, 'Failed to add reaction');
  }
});

// Remove reaction from message
socket.on('message:unreact', async (data) => {
  try {
    const { messageId, reactionId } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    const reaction = await getReaction(reactionId);
    if (!reaction) {
      return socket.notFound({ reactionId }, 'Reaction not found');
    }

    if (reaction.userId !== user.id) {
      return socket.forbidden({ reactionId }, 'Can only remove your own reactions');
    }

    await removeReaction(reactionId);

    // Broadcast reaction removal
    const message = await getMessage(messageId);
    io.to(message.roomId).emit('message:reaction_removed', {
      messageId,
      userId: user.id,
      reactionId,
    });

    socket.ok({ reactionId }, 'Reaction removed');
  } catch (error) {
    socket.error(error, 'Failed to remove reaction');
  }
});

Typing Indicators

Typing Events

javascript
// Start typing indicator
socket.on('typing:start', (data) => {
  try {
    const { roomId } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    if (!roomId) {
      return socket.badRequest({ requiredFields: ['roomId'] }, 'Room ID is required');
    }

    // Check if user is in room
    if (!socket.rooms.has(roomId)) {
      return socket.forbidden({ roomId }, 'Not a member of this room');
    }

    // Broadcast typing indicator to room (except sender)
    socket.to(roomId).emit('typing:user_started', {
      userId: user.id,
      username: user.username,
      roomId,
    });

    // Set timeout to auto-stop typing after 5 seconds
    clearTimeout(socket.typingTimeout);
    socket.typingTimeout = setTimeout(() => {
      socket.to(roomId).emit('typing:user_stopped', {
        userId: user.id,
        username: user.username,
        roomId,
      });
    }, 5000);

    socket.ok({}, 'Typing indicator started');
  } catch (error) {
    socket.error(error, 'Failed to start typing indicator');
  }
});

// Stop typing indicator
socket.on('typing:stop', (data) => {
  try {
    const { roomId } = data;
    const user = socket.user;

    if (!user) return;

    // Clear timeout
    clearTimeout(socket.typingTimeout);

    // Broadcast stop typing to room
    socket.to(roomId).emit('typing:user_stopped', {
      userId: user.id,
      username: user.username,
      roomId,
    });

    socket.ok({}, 'Typing indicator stopped');
  } catch (error) {
    socket.error(error, 'Failed to stop typing indicator');
  }
});

File Sharing Events

File Upload Events

javascript
// File upload event
socket.on('file:upload', async (data) => {
  try {
    const { roomId, fileName, fileSize, fileType, fileData } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'User not authenticated');
    }

    // Validation
    if (!roomId || !fileName || !fileData) {
      return socket.badRequest(
        {
          requiredFields: ['roomId', 'fileName', 'fileData'],
        },
        'Missing required fields',
      );
    }

    // File size limit (10MB)
    if (fileSize > 10 * 1024 * 1024) {
      return socket.badRequest(
        {
          maxSize: '10MB',
          receivedSize: fileSize,
        },
        'File too large',
      );
    }

    // Allowed file types
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain'];
    if (!allowedTypes.includes(fileType)) {
      return socket.badRequest(
        {
          fileType,
          allowedTypes,
        },
        'File type not allowed',
      );
    }

    // Save file
    const file = await saveFile({
      fileName,
      fileSize,
      fileType,
      fileData,
      uploadedBy: user.id,
      roomId,
    });

    // Create file message
    const message = {
      id: generateMessageId(),
      roomId,
      userId: user.id,
      username: user.username,
      type: 'file',
      content: fileName,
      file: {
        id: file.id,
        name: fileName,
        size: fileSize,
        type: fileType,
        url: file.url,
      },
      timestamp: new Date(),
    };

    await saveMessage(message);

    // Broadcast file message
    io.to(roomId).emit('message:received', message);

    socket.ok(
      {
        messageId: message.id,
        fileId: file.id,
        fileUrl: file.url,
      },
      'File uploaded successfully',
    );
  } catch (error) {
    socket.error(error, 'Failed to upload file');
  }
});

Event Monitoring and Analytics

Event Tracking

javascript
// Event middleware for tracking
const trackEvent = (eventName) => {
  return (socket, data, next) => {
    const user = socket.user;

    // Log event
    console.log(`Event: ${eventName}`, {
      socketId: socket.id,
      userId: user?.id,
      username: user?.username,
      data: data,
      timestamp: new Date(),
    });

    // Send to analytics service
    if (process.env.NODE_ENV === 'production') {
      sendEventToAnalytics(eventName, {
        userId: user?.id,
        socketId: socket.id,
        data,
      });
    }

    next();
  };
};

// Apply tracking to events
socket.use(trackEvent('socket_event'));

// Custom event tracking
socket.on('message:send', trackEvent('message_sent'), handleMessageSend);
socket.on('room:join', trackEvent('room_joined'), handleRoomJoin);

This comprehensive event management system provides structured handling of all Socket.IO events with proper validation, error handling, and Response Handler integration.

Released under the ISC License.