Socket IO Wrapper Utility Class

March 2, 2025

SocketIO

Socket IO Wrapper Utility Class

Thuta Sann

Thuta Sann

Socket.io wrapper utility class with strongly type support


Share This Snippet To :

Socket.io wrapper utility class with strongly type support

//--------- socket-payload.type.ts /** * Notification Payload * @description Notification payload * @author [thutasann](https://github.com/thutasann) */ export type NotificationPayload = { id: string; type: 'info' | 'success' | 'warning' | 'error'; message: string; timestamp: Date; }; /** * Error Payload * @description Error payload * @author [thutasann](https://github.com/thutasann) */ export type ErrorPayload = { code: string; message: string; details?: Record<string, any>; }; /** * Status Payload * @description Status payload * @author [thutasann](https://github.com/thutasann) */ export type StatusPayload = { type: string; status: 'online' | 'offline' | 'away'; lastSeen?: Date; }; /** * Message Payload * @description Message payload * @author [thutasann](https://github.com/thutasann) */ export type MessagePayload = { id: string; content: string; timestamp: Date; sender: string; }; //--------- socket.type.ts import { Server, Socket } from 'socket.io'; import { ErrorPayload, MessagePayload, NotificationPayload, StatusPayload } from './socket-payload.type'; /** * Server to Client Events * @description Server to Client events * @todo Add more server to client events * @author [thutasann](https://github.com/thutasann) */ export interface ServerToClientEvents { notification: (data: NotificationPayload) => void; error: (error: ErrorPayload) => void; statusUpdate: (status: StatusPayload) => void; } /** * Client to Server Events * @description Client to Server events * @todo Add more client to server events * @author [thutasann](https://github.com/thutasann) */ export interface ClientToServerEvents { subscribe: (channel: string) => void; unsubscribe: (channel: string) => void; message: (data: MessagePayload) => void; } /** * Inter-Server Events * @description Inter-Server events * @todo Add more inter-server events * @author [thutasann](https://github.com/thutasann) */ export interface InterServerEvents { ping: () => void; broadcast: (event: string, data: any) => void; } /** * Socket Data * @description Socket data * @todo Add more socket data properties * @author [thutasann](https://github.com/thutasann) */ export interface SocketData { userId: string; username: string; connectedAt: Date; } /** * Typed Server * @description Typed Server that extends the Socket.IO Server with * - Client to Server Events * - Server to Client Events * - Inter Server Events * @author [thutasann](https://github.com/thutasann) */ export type TypedServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>; /** * Typed Socket * @description Typed Server that extends the Socket.IO Server with * - Client to Server Events * - Server to Client Events * - Inter Server Events * - Socket Data * @author [thutasann](https://github.com/thutasann) */ export type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>; //--------- socket.service.ts import { Server as HTTPServer } from 'http'; import { ServerOptions, Server as SocketIOServer } from 'socket.io'; import { createAdapter } from 'socket.io-redis'; import { getRedisUrl } from '../../../core/connections/redis-connection'; import { MessagePayload } from '../../types/socket/socket-payload.type'; import { ClientToServerEvents, InterServerEvents, ServerToClientEvents, SocketData, TypedServer, TypedSocket, } from '../../types/socket/socket.type'; import { logger } from '../utils/logger'; /** * Socket Service that extends the Socket.IO Server * @description Socket Service that extends the Socket.IO Server */ class SocketService { private static instance: SocketService; private io: TypedServer | null = null; private readonly defaultOptions: Partial<ServerOptions> = { cors: { origin: '*', methods: ['GET', 'POST'], }, transports: ['websocket'], pingInterval: 25000, pingTimeout: 120000, upgradeTimeout: 30000, maxHttpBufferSize: 1e7, }; private constructor() {} // Empty private constructor /** * Initialize Socket Service * @description Initialize the Socket.IO Server with HTTP server * This should be called once when starting your application */ public initialize(server: HTTPServer): void { if (this.io) { logger.warning('Socket.IO server is already initialized'); return; } this.io = new SocketIOServer<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>( server, this.defaultOptions ); const redisUrl = getRedisUrl(); this.io.adapter(createAdapter(redisUrl)); this.initializeMiddleware(); this.start(); } /** * Get Socket Service Instance * @returns The Socket Service instance */ public static getInstance(): SocketService { if (!SocketService.instance) { SocketService.instance = new SocketService(); } return SocketService.instance; } /** * Ensure IO is initialized */ private ensureIOInitialized(): TypedServer { if (!this.io) { throw new Error('Socket.IO server is not initialized. Call initialize() first.'); } return this.io; } /** * Initialize Middleware * @description Initialize the middleware for the Socket.IO Server * @author [thutasann](https://github.com/thutasann) */ private initializeMiddleware(): void { const io = this.ensureIOInitialized(); io.use(async (socket, next) => { try { const token = socket.handshake.auth['token'] || socket.handshake.headers.authorization; if (!token) { logger.error('Socket Authentication token required'); throw new Error('Authentication token required'); } socket.data.userId = 'user_id'; socket.data.username = 'username'; socket.data.connectedAt = new Date(); next(); } catch (error) { next(error instanceof Error ? error : new Error('Unknown error')); } }); } /** * Handle Disconnect * @description Handle the disconnect event for the Socket.IO Server * @author [thutasann](https://github.com/thutasann) */ private handleDisconnect(socket: TypedSocket): void { socket.rooms.forEach((room) => { socket.leave(room); }); } /** * Handle Client Events * @description Handle the client events for the Socket.IO Server * - Subscribe to a channel * - Unsubscribe from a channel * - Send a message to a channel */ private handleClientEvents(socket: TypedSocket): void { socket.on('subscribe', (channel) => { socket.join(channel); logger.info(`Client ${socket.id} subscribed to ${channel}`); }); socket.on('unsubscribe', (channel) => { socket.leave(channel); logger.info(`Client ${socket.id} unsubscribed from ${channel}`); }); socket.on('message', (data) => { this.handleMessage(socket, data); }); } /** * Handle Message * @description Handle the message event for the Socket.IO Server */ private handleMessage(socket: TypedSocket, data: MessagePayload): void { try { const io = this.ensureIOInitialized(); io.to(data.sender).emit('notification', { id: Date.now().toString(), type: 'info', message: data.content, timestamp: new Date(), }); } catch (error) { socket.emit('error', { code: 'MESSAGE_ERROR', message: 'Failed to process message', details: { error: error instanceof Error ? error.message : 'Unknown error' }, }); } } /** * Start Socket Server * @description Start the Socket.IO Server */ public start(): void { try { const io = this.ensureIOInitialized(); io.on('connection', (socket: TypedSocket) => { logger.info(`New Client Connected: ${socket.id}`); this.handleClientEvents(socket); socket.on('disconnect', () => { logger.warning(`Client Disconnected: ${socket.id}`); this.handleDisconnect(socket); }); }); logger.success('Socket Initialized'); } catch (error) { logger.error(`Socket Initialization Failed: ${error}`); } } /** * Emit to All * @description Emit to all clients * @param event - The event to emit * @param args - The arguments to emit * @example * this.socketService.emitToAll('notification', { * id: Date.now().toString(), * type: 'info', * message: 'Hello, world!', * timestamp: new Date(), * }); */ public emitToAll<T extends keyof ServerToClientEvents>(event: T, ...args: Parameters<ServerToClientEvents[T]>) { const io = this.ensureIOInitialized(); io.emit(event, ...args); } /** * Emit to Specific Client * @description Emit to a specific client * @param socketId - The socket ID of the client to emit to * @param event - The event to emit * @param args - The arguments to emit * @example * this.socketService.emitToClient('123', 'notification', { * id: Date.now().toString(), * type: 'info', * message: 'Hello, world!', * timestamp: new Date(), * }); */ public emitToClient<T extends keyof ServerToClientEvents>( socketId: string, event: T, ...args: Parameters<ServerToClientEvents[T]> ): void { const io = this.ensureIOInitialized(); io.to(socketId).emit(event, ...args); } /** * Get Connected Clients * @description Get the connected clients * @param room - The room to get the connected clients from * @returns The connected clients * @example * this.socketService.getConnectedClients('room1'); */ public async getConnectedClients(room?: string): Promise<string[]> { const io = this.ensureIOInitialized(); if (room) { const sockets = await io.in(room).allSockets(); return Array.from(sockets); } const sockets = await io.allSockets(); return Array.from(sockets); } /** * Get Socket Instance by ID * @description Get the socket instance by ID * @param socketId - The socket ID of the client to get * @returns The socket instance * @example * this.socketService.getSocket('123'); */ public getSocket(socketId: string): TypedSocket | undefined { const io = this.ensureIOInitialized(); return io.sockets.sockets.get(socketId) as TypedSocket; } /** * Get IO Instance * @description Get the IO instance * @returns The IO instance * @example * this.socketService.getIO(); */ public getIO(): TypedServer { return this.ensureIOInitialized(); } } /** * Socket Service Instance * @description Get the Socket Service instance */ export const socketService = SocketService.getInstance();

Cookie

I baked some cookies that you have to accept, if you want to enjoy my portfolio.
In order to gather information and make improvements, I should use some third-party cookies too, Can I?