Socket.IO -- API Reference
Architecture deep dive, method signatures, internals, and type definitions.
Architecture
The component integrates Socket.IO into the Ignis application lifecycle with runtime-specific initialization (Node.js vs Bun).
Architecture Diagram
SocketIOComponent
+----------------------------------------------+
| |
| binding() |
| |-- resolveBindings() |
| | |-- SERVER_OPTIONS |
| | |-- REDIS_CONNECTION |
| | |-- AUTHENTICATE_HANDLER |
| | |-- VALIDATE_ROOM_HANDLER |
| | +-- CLIENT_CONNECTED_HANDLER |
| | |
| +-- RuntimeModules.detect() |
| |-- BUN -> registerBunHook() |
| +-- NODE -> registerNodeHook() |
| |
| (Post-start hooks execute after server) |
| |-- Creates SocketIOServerHelper |
| |-- await socketIOHelper.configure() |
| |-- Binds to SOCKET_IO_INSTANCE |
| +-- Wires into server (runtime-specific) |
+----------------------------------------------+Lifecycle Integration
The component uses the post-start hook system to solve a fundamental timing problem: Socket.IO needs a running server instance, but components are initialized before the server starts.
Application Lifecycle Diagram
Application Lifecycle
=====================
+------------------+
| preConfigure() | <-- Register SocketIOComponent here
+--------+---------+
|
+--------v---------+
| initialize() | <-- Component.binding() runs here
| | Resolves bindings, registers post-start hook
+--------+---------+
|
+--------v---------+
| setupMiddlewares |
+--------+---------+
|
+--------v-----------------------+
| startBunModule() OR | <-- Server starts, instance created
| startNodeModule() |
+--------+-----------------------+
|
+--------v--------------------------+
| executePostStartHooks() | <-- SocketIOServerHelper created HERE
| +-- socket-io-initialize | Server instance is now available
+-----------------------------------+Runtime-Specific Behavior
| Aspect | Node.js | Bun |
|---|---|---|
| Server Type | node:http.Server | Bun.Server |
| IO Server Init | new IOServer(httpServer, opts) | new IOServer() + io.bind(engine) |
| Engine | Built-in (socket.io) | @socket.io/bun-engine (optional peer dep) |
| Request Routing | Socket.IO attaches to HTTP server automatically | server.reload({ fetch, websocket }) wires engine into Bun's request loop |
| WebSocket Upgrade | Handled by node:http.Server upgrade event | Handled by Bun's websocket handler |
| Dynamic Import | None needed | await import('@socket.io/bun-engine') at runtime |
| Fetch Handler | Not needed -- HTTP server handles upgrades | Custom fetch wraps Hono fetch, routes WS upgrades to engine |
| CORS | Handled by socket.io CORS options | Handled by Bun engine options (requires explicit field bridging) |
| Server Access | Direct -- Socket.IO attaches to HTTP server | server.reload({ fetch, websocket }) to hot-swap handlers |
Runtime Differences -- Deep Dive
Bun Runtime
The Bun handler creates a custom fetch function that intercepts WebSocket upgrade requests:
- Checks if the request path matches the Socket.IO path (
serverOptions.path, default'/io') - If yes, delegates to
@socket.io/bun-engineviaengine.handleRequest(req, server)for WebSocket protocol handling - If no, delegates to Hono's normal
server.fetch(req, server)handler
Bun Fetch Handler Source
function createBunFetchHandler(opts: {
engine: any;
enginePath: string;
honoServer: OpenAPIHono;
}): (req: Request, server: TBunServerInstance) => Response | Promise<Response> {
const { engine, enginePath, honoServer } = opts;
return (req: Request, server: TBunServerInstance): Response | Promise<Response> => {
const url = new URL(req.url);
if (!url.pathname.startsWith(enginePath)) {
return honoServer.fetch(req, server);
}
return engine.handleRequest(req, server) ?? new Response(null, { status: 404 });
};
}CORS type bridging: Socket.IO and @socket.io/bun-engine have slightly different CORS type definitions. The component extracts individual CORS fields explicitly to avoid type mismatches without using as any:
Bun Engine CORS Bridging
const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
const engine = new BunEngine({
path: serverOptions.path ?? '/socket.io/',
...(corsConfig && {
cors: {
origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
methods: corsConfig.methods,
credentials: corsConfig.credentials,
allowedHeaders: corsConfig.allowedHeaders,
exposedHeaders: corsConfig.exposedHeaders,
maxAge: corsConfig.maxAge,
},
}),
});Node.js Runtime
Node mode is simpler because Socket.IO natively attaches to node:http.Server. The handler creates a SocketIOServerHelper with runtime: RuntimeModules.NODE and passes the HTTP server instance directly:
Node.js Handler Source
async function createNodeSocketIOHelper(opts: {
serverOptions: Partial<IServerOptions>;
httpServer: TNodeServerInstance;
resolvedBindings: IResolvedBindings;
}): Promise<SocketIOServerHelper> {
const { serverOptions, httpServer, resolvedBindings } = opts;
const { redisConnection, authenticateFn, validateRoomFn, clientConnectedFn } = resolvedBindings;
const socketIOHelper = new SocketIOServerHelper({
runtime: RuntimeModules.NODE,
identifier: serverOptions.identifier!,
server: httpServer,
serverOptions,
redisConnection,
authenticateFn,
validateRoomFn,
clientConnectedFn,
});
await socketIOHelper.configure();
return socketIOHelper;
}Server Helper API Reference
SocketIOServerHelper Constructor
The helper uses a discriminated union for its constructor options, keyed on runtime:
TSocketIOServerOptions Type
interface ISocketIOServerBaseOptions {
identifier: string;
serverOptions: Partial<ServerOptions>;
redisConnection: DefaultRedisHelper;
defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
authenticateTimeout?: number; // Default: 10_000 (10 seconds)
pingInterval?: number; // Default: 30_000 (30 seconds)
authenticateFn: TSocketIOAuthenticateFn;
validateRoomFn?: TSocketIOValidateRoomFn;
clientConnectedFn?: TSocketIOClientConnectedFn;
}
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
runtime: typeof RuntimeModules.NODE;
server: HTTPServer; // node:http.Server instance
}
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
runtime: typeof RuntimeModules.BUN;
engine: any; // @socket.io/bun-engine Server instance
}
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;During construction:
- Sets
identifier,runtime,serverOptions, callback functions - Sets defaults:
authenticateTimeout= 10s,pingInterval= 30s,defaultRooms=['io-default', 'io-notification'] - Calls
setRuntime()-- validates and stores the server or engine - Calls
initRedisClients()-- creates 3 duplicated Redis clients from the connection
IMPORTANT
Redis clients are duplicated from the parent connection (client.duplicate()). This means the helper uses 3 independent connections (pub, sub, emitter) that inherit config from the parent but maintain separate state. The parent RedisHelper connection is not consumed.
configure() -- Server Initialization
The configure() method is the main initialization entry point, called after construction:
configure()
|-- Register error handlers on all 3 Redis clients
|-- Connect any clients in 'wait' status (lazyConnect mode)
|-- await Promise.all([redisPub.ready, redisSub.ready, redisEmitter.ready])
|-- initIOServer()
| |-- NODE: new IOServer(httpServer, serverOptions)
| +-- BUN: new IOServer() -> io.bind(bunEngine)
|-- io.adapter(createAdapter(redisPub, redisSub))
|-- emitter = new Emitter(redisEmitter)
+-- io.on('connection', onClientConnect)NOTE
The configure() method is async because it waits for all 3 Redis connections to be ready before proceeding. If any Redis client fails to connect, the error propagates and the server will not start.
Public Methods
getIOServer()
getIOServer(): IOServerReturns the underlying socket.io Server instance. Use this for direct access to Socket.IO APIs not exposed by the helper (e.g., io.of('/namespace'), io.fetchSockets()).
getEngine()
getEngine(): anyReturns the @socket.io/bun-engine instance. Throws if the runtime is Node.js ("Engine is only available for Bun runtime!").
getClients()
// Overloaded:
getClients(): Map<string, ISocketIOClient>
getClients(opts: { id: string }): ISocketIOClient | undefinedWhen called without arguments, returns the full client map. When called with { id }, returns the specific client entry or undefined if not found.
on()
on<HandlerArgsType extends unknown[] = unknown[], HandlerReturnType = void>(opts: {
topic: string;
handler: (...args: HandlerArgsType) => ValueOrPromise<HandlerReturnType>;
}): voidRegisters a server-level event handler on the IO server. Throws if topic is empty, handler is falsy, or the IO server is not initialized.
ping()
ping(opts: { socket: IOSocket; doIgnoreAuth: boolean }): voidSends a ping event to a specific client with { time: <ISO string> }. Behavior:
- If
socketis undefined, logs and returns - If client is not found in the client map, logs and returns
- If
doIgnoreAuthisfalseand the client is notauthenticated, disconnects the client - If
doIgnoreAuthistrue, sends the ping regardless of auth state
Used internally for the keep-alive interval after authentication. The doIgnoreAuth: true flag is used for the initial post-auth ping and the recurring interval.
disconnect()
disconnect(opts: { socket: IOSocket }): voidDisconnects a specific client and cleans up resources:
- Clears the ping interval (if set)
- Clears the authentication timeout
- Removes the client from the
clientsmap - Calls
socket.disconnect()on the underlying Socket.IO socket
If the socket is undefined or not tracked in the client map, the method still calls socket.disconnect() for safety.
onClientConnect()
onClientConnect(opts: { socket: IOSocket }): voidHandles a new socket connection. Called by the connection event handler on the IO server. This method is public so it can be invoked externally for testing or custom connection routing.
Behavior:
- Validates the socket exists (returns if
null/undefined) - Checks for duplicate connections by socket ID (returns if already tracked)
- Creates an
ISocketIOCliententry with stateUNAUTHORIZED - Starts the authentication timeout (
authenticateTimeoutms) - Registers
disconnecthandler on the socket - Registers
authenticatehandler viaregisterAuthHandler()
onClientAuthenticated()
onClientAuthenticated(opts: { socket: IOSocket }): voidCalled after successful authentication. This method is public so it can be invoked externally for testing or custom auth flows.
Behavior:
- Validates the socket and client entry exist
- Sets client state to
AUTHENTICATED - Sends an initial ping
- Joins default rooms (
io-default,io-notification) - Registers room handlers (
join,leave) - Starts the ping interval
- Emits
authenticatedevent to the client with{ id, time } - Invokes the
clientConnectedFncallback (if configured)
Messaging via send()
The send() method uses the Redis emitter for message delivery, enabling cross-instance broadcasting:
send(opts: {
destination?: string; // Socket ID, room name, or omit for broadcast
payload: {
topic: string; // Event name
data: any; // Event payload
};
doLog?: boolean; // Log the emission (default: false)
cb?: () => void; // Callback executed via setImmediate after emit
})Key behaviors:
- All messages are compressed via
emitter.compress(true) - If
destinationis provided and non-empty, sends viasender.to(destination).emit(topic, data) - If
destinationis omitted/empty, broadcasts to all connected clients viasender.emit(topic, data) - Callback (
cb) is executed asynchronously viasetImmediate(), not after delivery confirmation - Logging is opt-in (
doLog: true) to avoid noise in high-throughput scenarios
send() Silent Failure Behavior
The send() method silently returns (no error, no log) in these cases:
payloadis falsypayload.topicis falsypayload.datais falsy
This is a deliberate design choice for fire-and-forget messaging patterns where callers do not need to know if a message was dropped due to missing fields.
TIP
The emitter uses the Redis emitter client, so messages are delivered across all server instances in a horizontally-scaled deployment. This works even if the recipient is connected to a different server instance.
Shutdown
shutdown(): Promise<void>Gracefully shuts down the server:
- Iterates all tracked clients and clears their intervals/timeouts
- Disconnects each client socket
- Clears the client map
- Closes the IO server (async, wrapped in a Promise)
- Quits all 3 Redis connections (
redisPub,redisSub,redisEmitter)
Client Helper API Reference
SocketIOClientHelper extends BaseHelper and provides a managed Socket.IO client. It wraps the socket.io-client library with lifecycle callbacks, error-safe event subscription, and authentication state tracking.
Constructor
constructor(opts: ISocketIOClientOptions)ISocketIOClientOptions Interface
interface ISocketIOClientOptions {
identifier: string;
host: string;
options: IOptions;
// Lifecycle callbacks (all optional)
onConnected?: () => ValueOrPromise<void>;
onDisconnected?: (reason: string) => ValueOrPromise<void>;
onError?: (error: Error) => ValueOrPromise<void>;
onAuthenticated?: () => ValueOrPromise<void>;
onUnauthenticated?: (message: string) => ValueOrPromise<void>;
}IOptions Interface
interface IOptions extends SocketOptions {
path: string;
extraHeaders: Record<string | symbol | number, any>;
}IOptions extends SocketOptions from socket.io-client with two required fields:
path-- the Socket.IO endpoint path (must match the server'spathoption, e.g.,'/io')extraHeaders-- headers sent with every request, commonly used forauthorizationtokens
Constructor Behavior
- Calls
super({ scope: opts.identifier })to initializeBaseHelperwith scoped logging - Stores the
identifier,host,options, and all lifecycle callbacks - Immediately calls
configure()to create the socket and register internal handlers
configure()
configure(): voidCreates the socket.io-client Socket instance and registers all internal event handlers. If the client is already established (i.e., configure() was already called), logs a message and returns early.
Registered handlers:
| Event | Internal Behavior |
|---|---|
connect | Logs connection, invokes onConnected callback |
disconnect | Logs disconnection with reason, resets state to unauthorized, invokes onDisconnected callback |
connect_error | Logs the error, invokes onError callback |
authenticated | Logs auth data, sets state to authenticated, invokes onAuthenticated callback |
unauthenticated | Logs warning with auth data, resets state to unauthorized, invokes onUnauthenticated callback with the message |
ping | Logs debug-level ping received |
All lifecycle callbacks are wrapped in Promise.resolve(...).catch(...) to prevent callback errors from crashing the client.
getState()
getState(): TSocketIOClientStateReturns the current authentication state: 'unauthorized', 'authenticating', or 'authenticated'.
TSocketIOClientState Type
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'getSocketClient()
getSocketClient(): SocketReturns the raw socket.io-client Socket instance. Use this for direct access to Socket.IO client APIs not exposed by the helper (e.g., socket.io, socket.connected, socket.id).
authenticate()
authenticate(): voidInitiates the authentication handshake by emitting the authenticate event to the server. The server will validate credentials from the socket handshake (headers, query, auth object) and respond with authenticated or unauthenticated.
Guard conditions (no-op with warning log):
- Socket is not connected (
!this.client?.connected) - Current state is not
unauthorized(prevents double-auth or re-auth while authenticating)
On call:
- Sets state to
authenticating - Emits
SocketIOConstants.EVENT_AUTHENTICATE(value:'authenticate')
subscribe()
subscribe<T = unknown>(opts: {
event: string;
handler: TSocketIOEventHandler<T>;
ignoreDuplicate?: boolean; // default: true
}): voidSubscribes to a Socket.IO event with automatic error safety.
Guard conditions (no-op with warning log):
handleris falsyignoreDuplicateistrue(default) and the event already has listeners
Handler Wrapping Pattern
Handlers are wrapped in a dual try-catch that catches both synchronous throws and asynchronous rejections:
const wrappedHandler = (data: T) => {
try {
Promise.resolve(handler(data)).catch(error => {
logger.error('Handler error | event: %s | error: %s', event, error);
});
} catch (error) {
logger.error('Handler error | event: %s | error: %s', event, error);
}
};The outer try-catch handles synchronous throws from the handler. The .catch() on Promise.resolve() handles async rejections. This ensures handler errors never crash the client.
TSocketIOEventHandler<T> Type
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;Handlers can be synchronous (void) or asynchronous (Promise<void>). Both are handled correctly by the wrapping pattern.
subscribeMany()
subscribeMany(opts: {
events: Record<string, TSocketIOEventHandler>;
ignoreDuplicate?: boolean;
}): voidBatch subscribes to multiple events. Iterates over the events record and calls subscribe() for each entry.
unsubscribe()
unsubscribe(opts: { event: string; handler?: TSocketIOEventHandler }): voidRemoves event listeners. If handler is provided, removes only that specific handler via socket.off(event, handler). If handler is omitted, removes all handlers for the event via socket.off(event).
No-op if the socket has no listeners for the event.
unsubscribeMany()
unsubscribeMany(opts: { events: string[] }): voidRemoves all handlers for each event in the array. Calls unsubscribe({ event }) for each entry.
connect()
connect(): voidManually connects the socket. No-op with an info log if the client is not initialized. Useful when autoConnect: false is set in the options.
disconnect()
disconnect(): voidManually disconnects the socket. No-op with an info log if the client is not initialized.
emit()
emit<T = unknown>(opts: {
topic: string;
data: T;
doLog?: boolean; // default: false
cb?: () => void;
}): voidEmits an event to the server.
Throws (via getError()) if:
- The socket is not connected (
statusCode: 400, message:"Invalid socket client state to emit") - The
topicis falsy (statusCode: 400, message:"Topic is required to emit")
If cb is provided, it is executed via setImmediate() (asynchronously, not after server acknowledgment). If doLog is true, logs the topic and data.
joinRooms()
joinRooms(opts: { rooms: string[] }): voidEmits a join event to the server with { rooms }. The server will validate via validateRoomFn and perform the actual join.
No-op with warning log if the socket is not connected.
leaveRooms()
leaveRooms(opts: { rooms: string[] }): voidEmits a leave event to the server with { rooms }. The server performs the actual leave without validation.
No-op with warning log if the socket is not connected.
shutdown()
shutdown(): voidClean shutdown of the client:
- Calls
removeAllListeners()on the underlying socket to prevent memory leaks - Disconnects if still connected
- Resets state to
unauthorized
Internals
resolveBindings()
Reads all binding keys from the DI container and validates required ones:
| Binding | Validation | Error on Failure |
|---|---|---|
SERVER_OPTIONS | Optional, merged with defaults via Object.assign() | -- |
REDIS_CONNECTION | Must be instanceof DefaultRedisHelper | `"Invalid instance of redisConnection |
AUTHENTICATE_HANDLER | Must be a function (non-null) | "[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!" |
VALIDATE_ROOM_HANDLER | Optional, resolved from container, null coerced to undefined | -- |
CLIENT_CONNECTED_HANDLER | Optional, resolved from container, null coerced to undefined | -- |
registerBunHook()
Registers a post-start hook that:
- Calls
createBunEngine({ serverOptions })which dynamically imports@socket.io/bun-engineand creates aBunEngineinstance with CORS config bridging - Creates
SocketIOServerHelperwithruntime: RuntimeModules.BUN - Awaits
socketIOHelper.configure()which waits for all Redis connections to be ready before initializing the adapter and emitter - Binds the helper to
SOCKET_IO_INSTANCE - Gets the Bun server instance and Hono server, then calls
serverInstance.reload()to wire the engine'sfetchandwebsockethandlers into the running Bun server
createBunEngine() Function
async function createBunEngine(opts: {
serverOptions: Partial<ServerOptions>;
}): Promise<{ engine: any; engineHandler: any }>Dynamically imports @socket.io/bun-engine, creates a BunEngine with CORS bridging, and returns both the engine and the engineHandler (from engine.handler()). The engineHandler provides the websocket handler that Bun's server needs.
createBunFetchHandler() Function
function createBunFetchHandler(opts: {
engine: any;
enginePath: string;
honoServer: OpenAPIHono;
}): (req: Request, server: TBunServerInstance) => Response | Promise<Response>Returns a fetch handler function that routes requests:
- If
url.pathnamestarts withenginePath, delegates toengine.handleRequest(req, server)(returns 404 Response ifhandleRequestreturns nullish) - Otherwise, delegates to
honoServer.fetch(req, server)for normal Hono routing
registerNodeHook()
Registers a post-start hook that:
- Gets the HTTP server instance via
getServerInstance() - Validates the server instance exists (throws
"[SocketIOComponent] HTTP server not available for Node.js runtime!"if not) - Calls
createNodeSocketIOHelper()which createsSocketIOServerHelperwithruntime: RuntimeModules.NODEand the HTTP server, then awaitsconfigure() - Binds the helper to
SOCKET_IO_INSTANCE
Node mode is simpler because Socket.IO natively attaches to node:http.Server.
Redis 3-Client Architecture
The server helper creates 3 independent Redis connections from a single DefaultRedisHelper:
RedisHelper (parent -- NOT consumed)
|
+-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
|
+-- client.duplicate() --> redisSub (for Redis adapter -- subscribes)
|
+-- client.duplicate() --> redisEmitter (for @socket.io/redis-emitter -- message delivery)Why 3 clients?
@socket.io/redis-adapterrequires separate pub and sub clients because a Redis connection in subscribe mode cannot execute other commands@socket.io/redis-emitteruses its own client to emit messages independently of the adapter, enabling cross-instance broadcasting even from contexts without a direct Socket.IO reference- The parent
RedisHelperconnection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
TRedisClient type:
type TRedisClient = Redis | Cluster;This supports both single-instance Redis and Cluster connections from ioredis, making the helper transparent to the Redis deployment topology.
setRuntime() -- Runtime Validation
The private setRuntime() method validates the constructor options based on the runtime discriminant:
| Runtime | Required Field | Error on Missing |
|---|---|---|
RuntimeModules.NODE | opts.server (HTTPServer) | "Invalid HTTP server for Node.js runtime!" |
RuntimeModules.BUN | opts.engine (BunEngine) | "Invalid @socket.io/bun-engine instance for Bun runtime!" |
| Other | -- | "Unsupported runtime!" |
initRedisClients() -- Redis Initialization
private initRedisClients(redisConnection: TSocketIOServerOptions['redisConnection']): voidThrows if redisConnection is falsy: "Invalid redis connection to config socket.io adapter!"
Creates 3 duplicated clients from the parent connection's underlying ioredis client.
initIOServer() -- IO Server Initialization
Called during configure() after Redis connections are ready:
| Runtime | Initialization |
|---|---|
RuntimeModules.NODE | this.io = new IOServer(this.server, this.serverOptions) |
RuntimeModules.BUN | this.io = new IOServer() then this.io.bind(this.bunEngine) |
| Other | Throws "Unsupported runtime: <runtime>" |
Additional validation errors:
- Node.js without
this.server:"[DANGER] Invalid HTTP server instance to init Socket.io server!" - Bun without
this.bunEngine:"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"
Connection Lifecycle
When a client connects, the server manages a strict authentication flow:
Client connects
|
+-- onClientConnect({ socket })
| +-- Validate socket exists and not duplicate
| +-- Create ISocketIOClient entry (state: UNAUTHORIZED)
| +-- Start authenticateTimeout (10s default)
| +-- Register 'disconnect' handler
| +-- Register 'authenticate' handler
|
+-- Client emits 'authenticate'
| +-- Validate client exists and state is UNAUTHORIZED
| +-- Set state to AUTHENTICATING
| +-- Call authenticateFn(handshake)
| +-- Success -> onClientAuthenticated()
| | +-- Set state to AUTHENTICATED
| | +-- Send initial ping
| | +-- Join default rooms (io-default, io-notification)
| | +-- Register 'join' and 'leave' room handlers
| | +-- Start ping interval (30s default)
| | +-- Emit 'authenticated' with { id, time }
| | +-- Call clientConnectedFn({ socket }) if provided
| +-- Failure -> emit 'unauthenticated' -> disconnect
|
+-- Timeout (10s) -> disconnect if not AUTHENTICATEDAuthentication Failure -- Two Code Paths
The registerAuthHandler() method handles authentication results through two distinct code paths:
Path 1: authenticateFn returns false (.then() handler):
- Sets client state back to
UNAUTHORIZED - Sends
unauthenticatedevent with message:"Invalid token to authenticate! Please login again!" - Disconnects after send via
setImmediatecallback - No error logging (this is an expected outcome)
Path 2: authenticateFn throws an error (.catch() handler):
- Sets client state back to
UNAUTHORIZED - Logs the error at error level
- Sends
unauthenticatedevent with message:"Failed to authenticate connection! Please login again!" - Sets
doLog: trueon the send call (unlike Path 1) - Disconnects after send via
setImmediatecallback
Both paths also handle the edge case where the client disconnected during authentication -- they check this.clients.has(id) before proceeding.
ISocketIOClient Interface
interface ISocketIOClient {
id: string;
socket: IOSocket;
state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
interval?: NodeJS.Timeout; // Ping interval (set after auth)
authenticateTimeout: NodeJS.Timeout; // Auth deadline (cleared on success)
}Room Handlers
Room join/leave handlers are registered after successful authentication:
join: Client emits{ rooms: string[] }. IfvalidateRoomFnis configured, only the rooms it returns are joined. IfvalidateRoomFnis not configured, join is silently rejected with a warning log.leave: Client emits{ rooms: string[] }. Leave is always allowed -- no validation function needed.
Both handlers parse the payload defensively: const { rooms = [] } = payload || { rooms: [] }. Empty rooms arrays are silently ignored.
Join handler validation errors are caught and logged but do not disconnect the client.
WARNING
Without a validateRoomFn bound, clients cannot join any custom rooms. They will only be in the default rooms (io-default, io-notification). This is a security-by-default design.
Types Reference
Server Types
// Server constructor options -- discriminated union on 'runtime'
type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
// Base options shared by both runtimes
interface ISocketIOServerBaseOptions {
identifier: string;
serverOptions: Partial<ServerOptions>;
redisConnection: DefaultRedisHelper;
defaultRooms?: string[];
authenticateTimeout?: number;
pingInterval?: number;
authenticateFn: TSocketIOAuthenticateFn;
validateRoomFn?: TSocketIOValidateRoomFn;
clientConnectedFn?: TSocketIOClientConnectedFn;
}
// Node.js runtime variant
interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
runtime: typeof RuntimeModules.NODE;
server: HTTPServer;
}
// Bun runtime variant
interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
runtime: typeof RuntimeModules.BUN;
engine: any;
}
// Tracked client entry (server-side)
interface ISocketIOClient {
id: string;
socket: IOSocket;
state: TSocketIOClientState;
interval?: NodeJS.Timeout;
authenticateTimeout: NodeJS.Timeout;
}
// Redis client type alias
type TRedisClient = Redis | Cluster;Client Types
// Client constructor options
interface ISocketIOClientOptions {
identifier: string;
host: string;
options: IOptions;
onConnected?: () => ValueOrPromise<void>;
onDisconnected?: (reason: string) => ValueOrPromise<void>;
onError?: (error: Error) => ValueOrPromise<void>;
onAuthenticated?: () => ValueOrPromise<void>;
onUnauthenticated?: (message: string) => ValueOrPromise<void>;
}
// Socket connection options (extends socket.io-client's SocketOptions)
interface IOptions extends SocketOptions {
path: string;
extraHeaders: Record<string | symbol | number, any>;
}
// Event handler type (supports sync and async)
type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
// Client state type
type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
// Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'Callback Types
// Server authentication handler
type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
// Server room validation handler
type TSocketIOValidateRoomFn = (opts: {
socket: IOSocket;
rooms: string[];
}) => ValueOrPromise<string[]>;
// Server client connected handler
type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;Component Types
// Extended ServerOptions with identifier
interface IServerOptions extends ServerOptions {
identifier: string;
}
// Resolved binding values from DI container
interface IResolvedBindings {
redisConnection: DefaultRedisHelper;
authenticateFn: TSocketIOAuthenticateFn;
validateRoomFn?: TSocketIOValidateRoomFn;
clientConnectedFn?: TSocketIOClientConnectedFn;
}Post-Start Hook System
The Socket.IO component uses post-start hooks to solve a timing problem: Socket.IO needs a running server, but components initialize before the server starts.
The component relies on AbstractApplication's post-start hook system:
API
// Register a hook (during binding phase)
application.registerPostStartHook({
identifier: string, // Unique name for logging
hook: () => ValueOrPromise<void>, // Async function to execute
});
// Get the server instance (available after start)
application.getServerInstance<T>(): T | undefined;Hook Execution Flow
Application.start()
|
+-- Bun.serve() / serve() <-- Server created
|
+-- executePostStartHooks() <-- Hooks run here
|
+-- SocketIOComponent hook:
1. Get server instance via getServerInstance()
2. Create SocketIOServerHelper with runtime-specific options
3. Call helper.configure() to initialize Socket.IO server
4. Bind the helper instance for injectionDetailed Hook Timing
executePostStartHooks()
|-- Hook 1: "socket-io-initialize"
| |-- performance.now() -> start
| |-- await hook()
| +-- log: "Executed hook | identifier: socket-io-initialize | took: 12.5 (ms)"
|-- Hook 2: "another-hook"
| +-- ...
+-- (hooks run sequentially in registration order)- Hooks run sequentially (not parallel) to guarantee ordering
- Each hook is timed with
performance.now()for diagnostics - If a hook throws, it propagates to
start()and the server fails to start
What Happens Inside the Hook
For Bun runtime, the hook:
- Calls
createBunEngine({ serverOptions })which dynamically imports@socket.io/bun-engineand creates aBunEngineinstance with CORS config bridging - Creates
SocketIOServerHelperwithruntime: RuntimeModules.BUNand the engine - Awaits
socketIOHelper.configure()which connects Redis pub/sub/emitter clients, initializes theIOServer, and sets up the Redis adapter - Binds the helper to
SOCKET_IO_INSTANCE - Gets the Bun server instance and Hono server
- Calls
serverInstance.reload({ fetch, websocket })to wire the engine's fetch and websocket handlers into the running Bun server, wherefetchis the result ofcreateBunFetchHandler()andwebsocketis fromengineHandler.websocket
For Node.js runtime, the hook:
- Gets the HTTP server instance via
getServerInstance() - Validates the server instance exists (throws if not)
- Calls
createNodeSocketIOHelper()which createsSocketIOServerHelperwithruntime: RuntimeModules.NODEand the HTTP server, then awaitsconfigure() - Binds the helper to
SOCKET_IO_INSTANCE
NOTE
The hook identifier is 'socket-io-initialize' for both runtimes. Only one runtime path executes per application.
Graceful Shutdown
Always shut down the Socket.IO server before stopping the application:
Shutdown Implementation
override async stop(): Promise<void> {
// 1. Shut down Socket.IO (disconnects all clients, closes IO server, quits Redis)
const socketIOHelper = this.get<SocketIOServerHelper>({
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
isOptional: true,
});
if (socketIOHelper) {
await socketIOHelper.shutdown();
}
// 2. Disconnect Redis helper
if (this.redisHelper) {
await this.redisHelper.disconnect();
}
// 3. Stop the HTTP/Bun server
await super.stop();
}Shutdown Flow
socketIOHelper.shutdown()
|-- Disconnect all tracked clients
| |-- clearInterval(ping)
| |-- clearTimeout(authenticateTimeout)
| +-- socket.disconnect()
|-- clients.clear()
|-- io.close() -- closes the Socket.IO server (async)
+-- Redis cleanup
|-- redisPub.quit()
|-- redisSub.quit()
+-- redisEmitter.quit()Client helper shutdown:
clientHelper.shutdown()
|-- removeAllListeners() -- prevents memory leaks
|-- disconnect() -- if still connected
+-- state = UNAUTHORIZEDSee Also
- Setup & Configuration -- Quick reference, installation, bindings, constants
- Usage & Examples -- Server-side usage, client helper, advanced patterns
- Error Reference -- Error conditions and troubleshooting