Usage & Examples
Task-oriented patterns for the Socket.IO component: full setup, sending messages from a service, using the standalone client helper, and reading the example app.
Full setup
1. Install dependencies
# Core dependency (already included via @venizia/ignis)
# ioredis is required for the Redis adapter
# Bun runtime only - optional peer dependency
bun add @socket.io/bun-engine2. Bind required and optional services
import { BaseApplication } from '@venizia/ignis';
import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
import { RedisSingleHelper, ValueOrPromise } from '@venizia/ignis-helpers';
import type {
TSocketIOAuthenticateFn,
TSocketIOValidateRoomFn,
TSocketIOClientConnectedFn,
} from '@venizia/ignis-helpers/socket-io';
export class Application extends BaseApplication {
private redisHelper: RedisSingleHelper;
preConfigure(): ValueOrPromise<void> {
this.setupSocketIO();
// ... other setup
}
setupSocketIO() {
// 1. Redis connection (required for adapter + emitter)
this.redisHelper = new RedisSingleHelper({
name: 'socket-io-redis',
host: process.env.REDIS_HOST ?? 'localhost',
port: +(process.env.REDIS_PORT ?? 6379),
password: process.env.REDIS_PASSWORD,
autoConnect: false,
});
this.bind<RedisSingleHelper>({
key: SocketIOBindingKeys.REDIS_CONNECTION,
}).toValue(this.redisHelper);
// 2. Authentication handler (required)
const authenticateFn: TSocketIOAuthenticateFn = handshake => {
const token = handshake.headers.authorization;
// Implement your auth logic: JWT verification, session check, etc.
return !!token;
};
this.bind<TSocketIOAuthenticateFn>({
key: SocketIOBindingKeys.AUTHENTICATE_HANDLER,
}).toValue(authenticateFn);
// 3. Room validation handler (optional - joins rejected without this)
const validateRoomFn: TSocketIOValidateRoomFn = ({ socket, rooms }) => {
// Return the rooms that the client is allowed to join
return rooms.filter(room => room.startsWith('public-'));
};
this.bind<TSocketIOValidateRoomFn>({
key: SocketIOBindingKeys.VALIDATE_ROOM_HANDLER,
}).toValue(validateRoomFn);
// 4. Client connected handler (optional)
const clientConnectedFn: TSocketIOClientConnectedFn = ({ socket }) => {
console.log('Client connected:', socket.id);
// Register custom event handlers on the socket
};
this.bind<TSocketIOClientConnectedFn>({
key: SocketIOBindingKeys.CLIENT_CONNECTED_HANDLER,
}).toValue(clientConnectedFn);
// 5. Register the component - that's it!
this.component(SocketIOComponent);
}
}3. Why autoConnect: false
The helper owns the connection timing, not you. RedisSingleHelper is created with autoConnect: false because the server helper calls duplicateClient() three times:
| Duplicate | Role |
|---|---|
redisPub | Redis adapter - publishes room broadcasts |
redisSub | Redis adapter - subscribes to room broadcasts |
redisEmitter | Redis emitter - direct cross-instance send |
- Duplicates inherit
lazyConnect, not connection state. Duringconfigure(), the helper checks each client's status. Any client stillwaiting getsconnect()called on it explicitly. The helper then waits for all three to reachreadybefore proceeding. - This avoids a race. If the parent connects before the duplicates exist, the duplicates can end up in a state inconsistent with the parent's connection lifecycle.
Redis connection alternatives
RedisSingleHelper (single instance), RedisClusterHelper (cluster mode), and RedisSentinelHelper (Sentinel HA) all extend AbstractRedisHelper and satisfy the IRedisHelper interface the component validates against.
import { RedisClusterHelper } from '@venizia/ignis-helpers';
// For Redis Cluster deployments
const redisHelper = new RedisClusterHelper({
name: 'socket-io-redis-cluster',
nodes: [
{ host: 'redis-node-1', port: 6379 },
{ host: 'redis-node-2', port: 6380 },
{ host: 'redis-node-3', port: 6381 },
],
password: process.env.REDIS_PASSWORD,
autoConnect: false,
});
this.bind<RedisClusterHelper>({
key: SocketIOBindingKeys.REDIS_CONNECTION,
}).toValue(redisHelper);The internal TRedisClient type is Redis | Cluster, so both ioredis connection types work transparently.
NOTE
Full defaults, the complete binding key table, and every system event/room constant are in the Full Reference.
Inject the helper in a service or controller
SocketIOServerHelper is bound to SOCKET_IO_INSTANCE inside a post-start hook. That hook runs after the server starts - well after the DI container already built every service and controller. Use a lazy getter that resolves the helper on first access. Never @inject it in a constructor.
import {
BaseService,
inject,
CoreBindings,
BaseApplication,
} from '@venizia/ignis';
import { SocketIOBindingKeys } from '@venizia/ignis/socket-io';
import { SocketIOServerHelper } from '@venizia/ignis-helpers/socket-io';
export class NotificationService extends BaseService {
private _io: SocketIOServerHelper | null = null;
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
private application: BaseApplication,
) {
super({ scope: NotificationService.name });
}
private get io(): SocketIOServerHelper {
if (!this._io) {
this._io = this.application.get<SocketIOServerHelper>({
key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
isOptional: true,
}) ?? null;
}
if (!this._io) {
throw new Error('SocketIO not initialized');
}
return this._io;
}
// Send to a specific client
notifyUser(opts: { userId: string; message: string }) {
this.io.send({
destination: opts.userId,
payload: {
topic: 'notification',
data: { message: opts.message, time: new Date().toISOString() },
},
});
}
// Send to a room
notifyRoom(opts: { room: string; message: string }) {
this.io.send({
destination: opts.room,
payload: { topic: 'room:update', data: { message: opts.message } },
});
}
// Broadcast to all clients
broadcastAnnouncement(opts: { message: string }) {
this.io.send({
payload: { topic: 'system:announcement', data: { message: opts.message } },
});
}
}- Never
@injectSOCKET_IO_INSTANCEin a constructor. It is not bound yet at that point. send()reads via the Redis emitter. It works even if the destination client is connected to a different server instance - seesend()in the Full Reference.
Use the client helper
SocketIOClientHelper wraps socket.io-client with authentication flow, lifecycle callbacks, and error-safe event subscription. Use it when your process needs to connect to a Socket.IO server, not run one - service-to-service communication, testing, or relay services.
Client setup
import { SocketIOClientHelper } from '@venizia/ignis-helpers/socket-io';
const client = new SocketIOClientHelper({
identifier: 'notification-relay',
host: 'http://localhost:3000',
options: {
path: '/io',
extraHeaders: { authorization: 'Bearer <token>' },
},
// Lifecycle callbacks (all optional)
onConnected: () => {
console.log('Connected to server');
client.authenticate();
},
onDisconnected: reason => console.log('Disconnected:', reason),
onError: error => console.error('Connection error:', error),
onAuthenticated: () => console.log('Authentication successful'),
onUnauthenticated: message => console.warn('Authentication failed:', message),
});- The constructor calls
configure()immediately. It creates thesocket.io-clientSocketinstance viaio(host, options)and registers the internal event handlers. See the full handler table in the Full Reference. - The socket connects on its own unless you disable it. Set
autoConnect: falseinoptionsand callclient.connect()yourself when you're ready.
connect vs connection event
Client and server fire different event names for the same moment:
| Side | Fires |
|---|---|
Client (socket.io-client) | connect - no suffix |
Server (socket.io) | connection - with the suffix |
This is a Socket.IO convention, not an IGNIS one. The client helper listens on 'connect'. The server helper listens on SocketIOConstants.EVENT_CONNECT, which equals 'connection'.
Authentication flow
After connecting, the client must emit authenticate to start the handshake. The server validates credentials from the socket handshake (headers, query params, auth object) and responds with either authenticated or unauthenticated.
client.authenticate();authenticate() is a no-op with a warning log unless both conditions hold:
- The socket is connected (
client.connected === true). - The current state is
unauthorized- callingauthenticate()whileauthenticatingor alreadyauthenticateddoes nothing.
Authentication failure messages
The server sends a different message depending on how authenticateFn failed. Both paths reset the client to unauthorized, emit unauthenticated with the message, and disconnect the socket after delivery (via setImmediate).
| Failure mode | Message |
|---|---|
authenticateFn returned false | "Invalid token to authenticate! Please login again!" |
authenticateFn threw an error | "Failed to authenticate connection! Please login again!" |
Event subscription
Handlers are wrapped in a dual try-catch. It catches both synchronous throws and asynchronous rejections, so a broken handler never crashes the client.
// Subscribe to a single event
client.subscribe({
event: 'chat:message',
handler: (data: { from: string; text: string }) => {
console.log(`${data.from}: ${data.text}`);
},
});
// ignoreDuplicate: false stacks a second handler for the same event
client.subscribe({
event: 'chat:message',
handler: data => { /* second handler */ },
ignoreDuplicate: false,
});
// Subscribe to multiple events at once
client.subscribeMany({
events: {
'user:joined': data => console.log('User joined:', data),
'user:left': data => console.log('User left:', data),
'room:updated': data => console.log('Room updated:', data),
},
});The default (ignoreDuplicate: true) checks socket.hasListeners(event) first. If a listener already exists, subscribe() is a no-op that logs an info message. Set ignoreDuplicate: false to stack handlers instead.
Unsubscribing
// Remove all handlers for an event
client.unsubscribe({ event: 'chat:message' });
// Remove a specific handler
client.unsubscribe({ event: 'chat:message', handler: myHandler });
// Remove handlers for multiple events
client.unsubscribeMany({ events: ['chat:message', 'user:joined', 'room:updated'] });Emitting events
client.emit({
topic: 'chat:send',
data: { text: 'Hello world' },
doLog: true, // optional: log the emission
callback: () => { // optional: invoked via setImmediate after emit
console.log('Message sent');
},
});emit() throws if the socket is not connected or if topic is missing. The server helper's send() silently drops a message with a missing field - emit() never does that. It always throws instead.
Room management
// Request to join rooms (server validates via validateRoomFn)
client.joinRooms({ rooms: ['chat-room-1', 'notifications'] });
// Request to leave rooms
client.leaveRooms({ rooms: ['chat-room-1'] });Both methods emit a Socket.IO event to the server: join or leave. The actual join or leave happens server-side. If the socket isn't connected, the call is a no-op with a warning log.
Connection management
// Manually connect (useful when autoConnect: false in options)
client.connect();
// Disconnect from server
client.disconnect();
// Check current state
const state = client.getState(); // 'unauthorized' | 'authenticating' | 'authenticated'
// Get raw socket.io-client Socket instance
const rawSocket = client.getSocketClient();Shutdown
client.shutdown();shutdown() does three things, in order:
- Calls
removeAllListeners()on the underlying socket to prevent memory leaks. - Disconnects if still connected.
- Resets state to
unauthorized.
Run the complete example
A full working example lives at examples/socket-io-test/.
| Feature | Implementation |
|---|---|
| Application setup | src/application.ts - bindings, component registration, graceful shutdown |
| REST endpoints | src/controllers/socket-test.controller.ts - 9 endpoints for Socket.IO management |
| Event handling | src/services/socket-event.service.ts - chat, echo, room management |
| Automated test client | client.ts - 15+ test cases covering all features |
REST API endpoints
| Method | Path | Description |
|---|---|---|
GET | /socket/info | Server status + connected client count |
GET | /socket/clients | List all connected client IDs |
GET | /socket/health | Health check (is SocketIO ready?) |
POST | /socket/broadcast | Broadcast {{ topic, data }} to all clients |
POST | /socket/room/{roomId}/send | Send {{ topic, data }} to a room |
POST | /socket/client/{clientId}/send | Send {{ topic, data }} to a specific client |
POST | /socket/client/{clientId}/join | Join client to {{ rooms: string[] }} |
POST | /socket/client/{clientId}/leave | Remove client from {{ rooms: string[] }} |
GET | /socket/client/{clientId}/rooms | List rooms a client belongs to |
Running the example
# Start the server
cd examples/socket-io-test
bun run server:dev
# In another terminal - run automated tests
bun client.tsThe automated client exercises:
- Authentication with valid and invalid tokens
- Ping/pong keepalive
- Room join/leave with validation
- Client-to-client messaging
- Room and global broadcasting
- The REST API
- Graceful disconnection
Read the example for these production-ready patterns:
- Binding multiple handlers in one
setupSocketIO()method - The lazy getter pattern for
SocketIOServerHelper - Custom event registration via
CLIENT_CONNECTED_HANDLER - Room validation that blocks unauthorized rooms
- A graceful shutdown sequence in
application.stop()
See also
- Overview - quick start, imports, common configuration tasks
- Full Reference - architecture, configuration reference, method signatures, internals, types
- Error Reference - error conditions and troubleshooting