@venizia/ignis-docs 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,122 +1,932 @@
1
- # Socket.IO Helper
1
+ # Socket.IO Helpers
2
2
 
3
- Structured Socket.IO client and server management for real-time bidirectional communication.
3
+ Structured Socket.IO server and client management for real-time bidirectional communication. Provides authentication, room management, Redis scaling, and lifecycle management out of the box.
4
4
 
5
5
  ## Quick Reference
6
6
 
7
- | Helper | Type | Features |
8
- |--------|------|----------|
9
- | **SocketIOServerHelper** | Server | Auth flow, room management, Redis scaling |
10
- | **SocketIOClientHelper** | Client | Structured API, event subscription |
7
+ | Helper | Package | Purpose |
8
+ |--------|---------|---------|
9
+ | [`SocketIOServerHelper`](#socketioserverhelper) | `@venizia/ignis-helpers` | Server-side Socket.IO wrapper with auth, rooms, Redis adapter |
10
+ | [`SocketIOClientHelper`](#socketioclienthelper) | `@venizia/ignis-helpers` | Client-side Socket.IO wrapper with structured event handling |
11
11
 
12
- ### SocketIOServerHelper Features
12
+ ### Import Paths
13
13
 
14
- | Feature | Description |
15
- |---------|-------------|
16
- | **Redis Integration** | `@socket.io/redis-adapter` (scaling), `@socket.io/redis-emitter` (broadcasting) |
17
- | **Authentication** | Built-in flow - clients emit `authenticate` event |
18
- | **Client Management** | Track connections and auth state |
19
- | **Room Management** | Group clients for targeted messaging |
14
+ ```typescript
15
+ // Server helper
16
+ import { SocketIOServerHelper } from '@venizia/ignis-helpers';
17
+
18
+ // Client helper
19
+ import { SocketIOClientHelper } from '@venizia/ignis-helpers';
20
+
21
+ // Types and constants
22
+ import {
23
+ TSocketIOServerOptions,
24
+ ISocketIOServerBaseOptions,
25
+ ISocketIOServerNodeOptions,
26
+ ISocketIOServerBunOptions,
27
+ ISocketIOClientOptions,
28
+ IHandshake,
29
+ ISocketIOClient,
30
+ SocketIOConstants,
31
+ SocketIOClientStates,
32
+ TSocketIOEventHandler,
33
+ TSocketIOAuthenticateFn,
34
+ TSocketIOValidateRoomFn,
35
+ TSocketIOClientConnectedFn,
36
+ } from '@venizia/ignis-helpers';
37
+ ```
38
+
39
+ ---
40
+
41
+ # SocketIOServerHelper
42
+
43
+ Wraps the Socket.IO `Server` instance with built-in authentication flow, client tracking, room management, Redis adapter/emitter, and dual-runtime support (Node.js + Bun).
44
+
45
+ ## Constructor
46
+
47
+ ```typescript
48
+ new SocketIOServerHelper(opts: TSocketIOServerOptions)
49
+ ```
50
+
51
+ ### Options (Discriminated Union)
52
+
53
+ `TSocketIOServerOptions` is a discriminated union on the `runtime` field:
54
+
55
+ ```typescript
56
+ type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
57
+ ```
58
+
59
+ **Base options** (shared by both runtimes):
60
+
61
+ | Field | Type | Required | Default | Description |
62
+ |-------|------|----------|---------|-------------|
63
+ | `identifier` | `string` | Yes | — | Unique name for this Socket.IO server instance |
64
+ | `serverOptions` | `Partial<ServerOptions>` | Yes | — | Socket.IO server configuration (path, cors, etc.) |
65
+ | `redisConnection` | `DefaultRedisHelper` | Yes | — | Redis helper for adapter + emitter. Creates 3 duplicate connections internally |
66
+ | `authenticateFn` | `TSocketIOAuthenticateFn` | Yes | — | Called when client emits `authenticate`. Return `true` to accept |
67
+ | `clientConnectedFn` | `TSocketIOClientConnectedFn` | No | — | Called after successful authentication |
68
+ | `validateRoomFn` | `TSocketIOValidateRoomFn` | No | — | Called when client requests to join rooms. Return allowed room names. Joins rejected if not provided |
69
+ | `authenticateTimeout` | `number` | No | `10000` (10s) | Milliseconds before unauthenticated clients are disconnected |
70
+ | `pingInterval` | `number` | No | `30000` (30s) | Milliseconds between keep-alive pings to authenticated clients |
71
+ | `defaultRooms` | `string[]` | No | `['io-default', 'io-notification']` | Rooms clients auto-join after authentication |
72
+
73
+ **Node.js-specific options** (`runtime: RuntimeModules.NODE`):
20
74
 
21
- ### Common Operations
75
+ | Field | Type | Required | Description |
76
+ |-------|------|----------|-------------|
77
+ | `runtime` | `typeof RuntimeModules.NODE` | Yes | Must be `RuntimeModules.NODE` (`'node'`) |
78
+ | `server` | `node:http.Server` | Yes | The HTTP server instance to attach Socket.IO to |
22
79
 
23
- | Helper | Method | Purpose |
24
- |--------|--------|---------|
25
- | **Server** | `send({ destination, payload })` | Send message to room/socket |
26
- | **Server** | `broadcast({ payload })` | Broadcast to all clients |
27
- | **Client** | `connect()` | Connect to server |
28
- | **Client** | `emit({ topic, ...data })` | Emit event |
29
- | **Client** | `subscribe({ events })` | Subscribe to events |
80
+ **Bun-specific options** (`runtime: RuntimeModules.BUN`):
30
81
 
31
- ### Usage
82
+ | Field | Type | Required | Description |
83
+ |-------|------|----------|-------------|
84
+ | `runtime` | `typeof RuntimeModules.BUN` | Yes | Must be `RuntimeModules.BUN` (`'bun'`) |
85
+ | `engine` | `any` | Yes | `@socket.io/bun-engine` Server instance |
32
86
 
33
- The `SocketIOServerHelper` is typically instantiated and managed by the `SocketIOComponent`. To use it, you need to provide the necessary configurations and handlers when you register the component. See the [Socket.IO Component documentation](../components/socket-io.md) for details on how to set it up.
87
+ ### Constructor Examples
34
88
 
35
- Once configured, you can inject the `SocketIOServerHelper` instance into your services or controllers to emit events.
89
+ **Node.js:**
36
90
 
37
91
  ```typescript
38
- import { SocketIOServerHelper, SocketIOBindingKeys, inject } from '@venizia/ignis';
92
+ import { RuntimeModules, SocketIOServerHelper } from '@venizia/ignis-helpers';
39
93
 
40
- // ... in a service or controller
94
+ const helper = new SocketIOServerHelper({
95
+ runtime: RuntimeModules.NODE,
96
+ identifier: 'my-socket-server',
97
+ server: httpServer, // node:http.Server
98
+ serverOptions: { path: '/io', cors: { origin: '*' } },
99
+ redisConnection: myRedisHelper,
100
+ authenticateFn: async (handshake) => {
101
+ const token = handshake.headers.authorization;
102
+ return verifyJWT(token);
103
+ },
104
+ clientConnectedFn: ({ socket }) => {
105
+ console.log('Client authenticated:', socket.id);
106
+ },
107
+ });
108
+ ```
109
+
110
+ **Bun:**
41
111
 
42
- @inject({ key: SocketIOBindingKeys.SOCKET_IO_INSTANCE })
43
- private _io: SocketIOServerHelper;
112
+ ```typescript
113
+ import { RuntimeModules, SocketIOServerHelper } from '@venizia/ignis-helpers';
114
+
115
+ const { Server: BunEngine } = await import('@socket.io/bun-engine');
116
+ const engine = new BunEngine({ path: '/io', cors: { origin: '*' } });
44
117
 
45
- sendNotification(opts: { userId: string; message: string }) {
46
- this._io.send({
47
- destination: opts.userId, // Room or socket ID
48
- payload: {
49
- topic: 'notification',
50
- data: { message: opts.message },
51
- },
52
- });
53
- }
118
+ const helper = new SocketIOServerHelper({
119
+ runtime: RuntimeModules.BUN,
120
+ identifier: 'my-socket-server',
121
+ engine, // @socket.io/bun-engine instance
122
+ serverOptions: { path: '/io', cors: { origin: '*' } },
123
+ redisConnection: myRedisHelper,
124
+ authenticateFn: async (handshake) => {
125
+ const token = handshake.headers.authorization;
126
+ return verifyJWT(token);
127
+ },
128
+ });
54
129
  ```
55
130
 
56
- ## `SocketIOClientHelper`
131
+ ---
132
+
133
+ ## Methods
57
134
 
58
- The `SocketIOClientHelper` provides a structured API for managing client-side Socket.IO connections.
135
+ ### `getIOServer(): IOServer`
59
136
 
60
- ### Creating a Socket.IO Client
137
+ Returns the raw Socket.IO `Server` instance for advanced operations.
61
138
 
62
139
  ```typescript
63
- import { SocketIOClientHelper } from '@venizia/ignis';
140
+ const io = helper.getIOServer();
141
+ io.of('/admin').on('connection', socket => { /* ... */ });
142
+ ```
143
+
144
+ ### `getClients(opts?): ISocketIOClient | Map<string, ISocketIOClient> | undefined`
145
+
146
+ Returns tracked clients.
147
+
148
+ | Parameter | Type | Description |
149
+ |-----------|------|-------------|
150
+ | `opts.id` | `string` (optional) | Specific client ID. If omitted, returns all clients |
151
+
152
+ ```typescript
153
+ // Get all clients
154
+ const allClients = helper.getClients() as Map<string, ISocketIOClient>;
155
+ console.log('Connected:', allClients.size);
156
+
157
+ // Get specific client
158
+ const client = helper.getClients({ id: socketId }) as ISocketIOClient | undefined;
159
+ if (client) {
160
+ console.log('State:', client.state); // 'authenticated' | 'authenticating' | 'unauthorized'
161
+ }
162
+ ```
163
+
164
+ ### `on<HandlerArgsType, HandlerReturnType>(opts): void`
165
+
166
+ Register a listener on the IO Server (not individual sockets).
167
+
168
+ | Parameter | Type | Description |
169
+ |-----------|------|-------------|
170
+ | `opts.topic` | `string` | Event name |
171
+ | `opts.handler` | `(...args) => ValueOrPromise<T>` | Event handler |
172
+
173
+ ```typescript
174
+ helper.on({
175
+ topic: 'connection',
176
+ handler: (socket) => {
177
+ console.log('New connection:', socket.id);
178
+ },
179
+ });
180
+ ```
181
+
182
+ ### `send(opts): void`
183
+
184
+ Send a message via the Redis emitter (works across processes/instances).
185
+
186
+ | Parameter | Type | Required | Description |
187
+ |-----------|------|----------|-------------|
188
+ | `opts.destination` | `string` | No | Socket ID or room name. If omitted, broadcasts to all |
189
+ | `opts.payload.topic` | `string` | Yes | Event name |
190
+ | `opts.payload.data` | `any` | Yes | Event payload |
191
+ | `opts.doLog` | `boolean` | No | Log the emission (default: `false`) |
192
+ | `opts.cb` | `() => void` | No | Callback executed via `setImmediate` after emit |
193
+
194
+ ```typescript
195
+ // Send to specific client
196
+ helper.send({
197
+ destination: socketId,
198
+ payload: { topic: 'notification', data: { message: 'Hello!' } },
199
+ });
200
+
201
+ // Send to room
202
+ helper.send({
203
+ destination: 'io-notification',
204
+ payload: { topic: 'alert', data: { level: 'warning', text: 'CPU high' } },
205
+ });
206
+
207
+ // Broadcast to all
208
+ helper.send({
209
+ payload: { topic: 'system:announcement', data: { text: 'Maintenance in 5 min' } },
210
+ });
211
+ ```
212
+
213
+ ### `disconnect(opts): void`
214
+
215
+ Disconnect a client and clean up resources.
216
+
217
+ | Parameter | Type | Description |
218
+ |-----------|------|-------------|
219
+ | `opts.socket` | `IOSocket` | The socket to disconnect |
220
+
221
+ ```typescript
222
+ helper.disconnect({ socket: clientSocket });
223
+ // Clears ping interval, auth timeout, removes from tracking, calls socket.disconnect()
224
+ ```
225
+
226
+ ### `getEngine(): any`
227
+
228
+ Returns the `@socket.io/bun-engine` instance (Bun runtime only). **Throws** if called on a non-Bun runtime.
229
+
230
+ ```typescript
231
+ const engine = helper.getEngine();
232
+ // Use for Bun-specific operations
233
+ // Throws an error if runtime is not Bun
234
+ ```
235
+
236
+ ### `shutdown(): Promise<void>`
237
+
238
+ Full graceful shutdown — disconnects all clients, closes server, quits Redis connections.
239
+
240
+ ```typescript
241
+ await helper.shutdown();
242
+ ```
243
+
244
+ **Shutdown sequence:**
245
+
246
+ ```
247
+ shutdown()
248
+
249
+ ├── For each tracked client:
250
+ │ ├── clearInterval(ping interval)
251
+ │ ├── clearTimeout(authenticate timeout)
252
+ │ └── socket.disconnect()
253
+
254
+ ├── clients.clear()
255
+
256
+ ├── io.close()
257
+
258
+ └── Quit Redis clients (parallel):
259
+ ├── redisPub.quit()
260
+ ├── redisSub.quit()
261
+ └── redisEmitter.quit()
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Authentication Flow
267
+
268
+ The server implements a challenge-response authentication pattern using Socket.IO events:
269
+
270
+ ```
271
+ Client Server (SocketIOServerHelper)
272
+ │ │
273
+ │── connect ──────────────────► │ onClientConnect()
274
+ │ │ ├── Create client entry (state: UNAUTHORIZED)
275
+ │ │ ├── Start authenticateTimeout (10s)
276
+ │ │ └── Register disconnect handler
277
+ │ │
278
+ │── "authenticate" ───────────► │ Event handler
279
+ │ │ ├── Set state: AUTHENTICATING
280
+ │ │ └── Call authenticateFn(handshake)
281
+ │ │
282
+ │ │ ── authenticateFn returns true ──
283
+ │ │ onClientAuthenticated()
284
+ │ │ ├── Set state: AUTHENTICATED
285
+ │ │ ├── Send initial ping
286
+ │ │ ├── Join default rooms
287
+ │ │ ├── Register join/leave handlers
288
+ │ │ ├── Start ping interval (configurable via `pingInterval`)
289
+ │ ◄── "authenticated" ──────── │ ├── Emit "authenticated" with { id, time }
290
+ │ │ └── Call clientConnectedFn({ socket })
291
+ │ │
292
+ │ │ ── authenticateFn returns false ──
293
+ │ ◄── "unauthenticated" ────── │ ├── Emit "unauthenticated" with error message
294
+ │ │ └── Disconnect client (via callback)
295
+ │ │
296
+ │ │ ── authenticateTimeout expires ──
297
+ │ │ └── Disconnect client (if still UNAUTHORIZED)
298
+ ```
299
+
300
+ ### `IHandshake` Object
301
+
302
+ The `authenticateFn` receives the Socket.IO handshake data:
303
+
304
+ | Field | Type | Description |
305
+ |-------|------|-------------|
306
+ | `headers` | `IncomingHttpHeaders` | HTTP headers from the connection request |
307
+ | `time` | `string` | Handshake timestamp |
308
+ | `address` | `string` | Client IP address |
309
+ | `xdomain` | `boolean` | Whether the request is cross-domain |
310
+ | `secure` | `boolean` | Whether the connection is secure (HTTPS/WSS) |
311
+ | `issued` | `number` | When the handshake was issued (Unix timestamp) |
312
+ | `url` | `string` | Request URL |
313
+ | `query` | `ParsedUrlQuery` | URL query parameters |
314
+ | `auth` | `Record<string, any>` | Auth data from the client's `auth` option |
315
+
316
+ ### `ISocketIOClient` Tracked State
317
+
318
+ Each connected client is tracked with:
319
+
320
+ | Field | Type | Description |
321
+ |-------|------|-------------|
322
+ | `id` | `string` | Socket ID |
323
+ | `socket` | `IOSocket` | Raw Socket.IO socket |
324
+ | `state` | `TSocketIOClientState` | Authentication state |
325
+ | `interval` | `NodeJS.Timeout?` | Ping interval (configurable via `pingInterval`) |
326
+ | `authenticateTimeout` | `NodeJS.Timeout` | Timeout to disconnect unauthenticated clients |
327
+
328
+ ### Client States
329
+
330
+ ```
331
+ ┌──────────────┐ authenticate ┌────────────────┐ auth success ┌───────────────┐
332
+ │ UNAUTHORIZED │ ──────────────────► │ AUTHENTICATING │ ────────────────► │ AUTHENTICATED │
333
+ └──────────────┘ └────────────────┘ └───────────────┘
334
+ ▲ │ │
335
+ │ auth failure │ │
336
+ └─────────────────────────────────────────┘ │
337
+ ▲ │
338
+ │ disconnect │
339
+ └──────────────────────────────────────────────────────────────────────────────┘
340
+ ```
341
+
342
+ | State | Value | Description |
343
+ |-------|-------|-------------|
344
+ | `SocketIOClientStates.UNAUTHORIZED` | `'unauthorized'` | Initial state, or after auth failure |
345
+ | `SocketIOClientStates.AUTHENTICATING` | `'authenticating'` | `authenticate` event received, awaiting `authenticateFn` |
346
+ | `SocketIOClientStates.AUTHENTICATED` | `'authenticated'` | Successfully authenticated, fully operational |
347
+
348
+ ---
349
+
350
+ ## Room Management
351
+
352
+ After authentication, clients can join and leave rooms:
353
+
354
+ ### Built-in Events
355
+
356
+ | Event | Direction | Payload | Description |
357
+ |-------|-----------|---------|-------------|
358
+ | `join` | Client → Server | `{ rooms: string[] }` | Join one or more rooms |
359
+ | `leave` | Client → Server | `{ rooms: string[] }` | Leave one or more rooms |
360
+
361
+ These handlers are registered automatically in `onClientAuthenticated()`.
362
+
363
+ ### Room Validation
364
+
365
+ Client room join requests are validated using the `validateRoomFn` callback. If no `validateRoomFn` is configured, **all join requests are rejected** for security.
366
+
367
+ ```typescript
368
+ const helper = new SocketIOServerHelper({
369
+ // ...
370
+ validateRoomFn: ({ socket, rooms }) => {
371
+ // Only allow rooms prefixed with 'public-' or the user's own room
372
+ const userId = socket.handshake.auth.userId;
373
+ return rooms.filter(room =>
374
+ room.startsWith('public-') || room === `user-${userId}`
375
+ );
376
+ },
377
+ });
378
+ ```
379
+
380
+ The function receives the socket and requested rooms, and must return the subset of rooms the client is allowed to join.
381
+
382
+ ### Default Rooms
383
+
384
+ Authenticated clients auto-join these rooms (configurable via `defaultRooms`):
385
+
386
+ | Room | Constant | Purpose |
387
+ |------|----------|---------|
388
+ | `io-default` | `SocketIOConstants.ROOM_DEFAULT` | General-purpose room for all clients |
389
+ | `io-notification` | `SocketIOConstants.ROOM_NOTIFICATION` | Notification delivery room |
390
+
391
+ ### Programmatic Room Management
392
+
393
+ From your service code, you can manage rooms via the tracked client's socket:
394
+
395
+ ```typescript
396
+ const client = helper.getClients({ id: socketId }) as ISocketIOClient | undefined;
397
+
398
+ if (client) {
399
+ // Join rooms
400
+ client.socket.join(['room-a', 'room-b']);
401
+
402
+ // Leave a room
403
+ client.socket.leave('room-a');
404
+
405
+ // Get current rooms
406
+ const rooms = Array.from(client.socket.rooms);
407
+ // rooms = [socketId, 'room-b', 'io-default', 'io-notification']
408
+ // Note: first room is always the socket's own ID
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Redis Integration
415
+
416
+ The helper creates **three** dedicated Redis connections (duplicated from your `redisConnection`):
417
+
418
+ | Connection | Purpose | Library |
419
+ |------------|---------|---------|
420
+ | `redisPub` | Publish adapter messages | `@socket.io/redis-adapter` |
421
+ | `redisSub` | Subscribe to adapter messages | `@socket.io/redis-adapter` |
422
+ | `redisEmitter` | Emit messages to other processes | `@socket.io/redis-emitter` |
423
+
424
+ ### Why Three Connections?
425
+
426
+ ```
427
+ Process A Redis Process B
428
+ ┌─────────┐ ┌──────────┐ ┌─────────┐
429
+ │ IO Server│──redisPub────►│ │◄──redisPub────│ IO Server│
430
+ │ │◄──redisSub────│ Pub/Sub │──redisSub────►│ │
431
+ │ │ │ │ │ │
432
+ │ Emitter │──redisEmitter►│ Streams │◄──redisEmitter│ Emitter │
433
+ └─────────┘ └──────────┘ └─────────┘
434
+ ```
435
+
436
+ - **Adapter** (pub/sub pair): Synchronizes Socket.IO state across multiple server instances. When server A emits to a room, the adapter broadcasts via Redis so server B's clients in that room also receive the event.
437
+ - **Emitter**: Allows emitting events from non-Socket.IO processes (background workers, microservices) using the same Redis connection.
438
+
439
+ ### Horizontal Scaling
440
+
441
+ With Redis adapter configured, you can run multiple server instances behind a load balancer:
442
+
443
+ ```
444
+ Client A ──► Load Balancer ──► Server 1 (Socket.IO + Redis Adapter)
445
+ Client B ──► │ ──► Server 2 (Socket.IO + Redis Adapter)
446
+ Client C ──► │ ──► Server 3 (Socket.IO + Redis Adapter)
447
+
448
+ All servers share state via Redis
449
+ ```
450
+
451
+ Events emitted via `helper.send()` use the **emitter** (not direct socket), so they propagate across all instances automatically.
452
+
453
+ ---
454
+
455
+ ## Built-in Events Reference
456
+
457
+ | Event | Constant | Direction | When |
458
+ |-------|----------|-----------|------|
459
+ | `connection` | `SocketIOConstants.EVENT_CONNECT` | Client → Server | New WebSocket connection established |
460
+ | `disconnect` | `SocketIOConstants.EVENT_DISCONNECT` | Client → Server | Client disconnects (intentional or timeout) |
461
+ | `authenticate` | `SocketIOConstants.EVENT_AUTHENTICATE` | Client → Server | Client requests authentication |
462
+ | `authenticated` | `SocketIOConstants.EVENT_AUTHENTICATED` | Server → Client | Authentication succeeded |
463
+ | `unauthenticated` | `SocketIOConstants.EVENT_UNAUTHENTICATE` | Server → Client | Authentication failed |
464
+ | `ping` | `SocketIOConstants.EVENT_PING` | Server → Client | Server → Client keep-alive (interval configurable via `pingInterval`) |
465
+ | `join` | `SocketIOConstants.EVENT_JOIN` | Client → Server | Request to join rooms |
466
+ | `leave` | `SocketIOConstants.EVENT_LEAVE` | Client → Server | Request to leave rooms |
467
+
468
+ ---
469
+
470
+ ## `configure()` Internals
471
+
472
+ The `configure()` method sets up the IO server based on runtime:
473
+
474
+ ```
475
+ configure() [async]
476
+
477
+ ├── Register error handlers on redisPub, redisSub, redisEmitter
478
+
479
+ ├── Await all Redis connections ready
480
+ │ └── Promise.all([waitForRedisReady(pub), waitForRedisReady(sub), waitForRedisReady(emitter)])
481
+
482
+ ├── Runtime check
483
+ │ ├── NODE: new IOServer(httpServer, serverOptions)
484
+ │ └── BUN: new IOServer() + io.bind(bunEngine)
485
+
486
+ ├── Redis Adapter
487
+ │ └── io.adapter(createAdapter(redisPub, redisSub))
488
+
489
+ ├── Redis Emitter
490
+ │ └── new Emitter(redisEmitter)
491
+
492
+ └── Connection handler
493
+ └── io.on('connection', socket => onClientConnect({ socket }))
494
+ ```
495
+
496
+ ---
497
+
498
+ # SocketIOClientHelper
499
+
500
+ Structured client-side Socket.IO connection management with lifecycle callbacks, event subscription, authentication, and room management.
501
+
502
+ ## Constructor
503
+
504
+ ```typescript
505
+ new SocketIOClientHelper(opts: ISocketIOClientOptions)
506
+ ```
64
507
 
65
- const socketClient = new SocketIOClientHelper({
66
- identifier: 'my-socket-client',
508
+ ### Options
509
+
510
+ | Field | Type | Required | Description |
511
+ |-------|------|----------|-------------|
512
+ | `identifier` | `string` | Yes | Unique name for this client |
513
+ | `host` | `string` | Yes | Server URL (e.g., `http://localhost:3000`) |
514
+ | `options` | `IOptions` | Yes | Socket.IO client options (path, extraHeaders, etc.) |
515
+ | `onConnected` | `() => ValueOrPromise<void>` | No | Called when WebSocket connection is established |
516
+ | `onDisconnected` | `(reason: string) => ValueOrPromise<void>` | No | Called on disconnect with reason |
517
+ | `onError` | `(error: Error) => ValueOrPromise<void>` | No | Called on connection error |
518
+ | `onAuthenticated` | `() => ValueOrPromise<void>` | No | Called when server sends `authenticated` event |
519
+ | `onUnauthenticated` | `(message: string) => ValueOrPromise<void>` | No | Called when server sends `unauthenticated` event |
520
+
521
+ ### `IOptions`
522
+
523
+ Extends `SocketOptions` from `socket.io-client`:
524
+
525
+ | Field | Type | Description |
526
+ |-------|------|-------------|
527
+ | `path` | `string` | Socket.IO server path (e.g., `'/io'`) |
528
+ | `extraHeaders` | `Record<string, any>` | HTTP headers sent with the connection (e.g., `Authorization`) |
529
+ | *...inherited* | | All `socket.io-client` `SocketOptions` |
530
+
531
+ ### Example
532
+
533
+ ```typescript
534
+ import { SocketIOClientHelper } from '@venizia/ignis-helpers';
535
+
536
+ const client = new SocketIOClientHelper({
537
+ identifier: 'my-client',
67
538
  host: 'http://localhost:3000',
68
539
  options: {
69
- path: '/io', // Path to the Socket.IO server
540
+ path: '/io',
70
541
  extraHeaders: {
71
542
  Authorization: 'Bearer my-jwt-token',
72
543
  },
73
- auth: {
74
- token: 'my-jwt-token',
75
- }
544
+ },
545
+ onConnected: () => {
546
+ console.log('Connected!');
547
+ client.authenticate(); // Trigger authentication flow
548
+ },
549
+ onAuthenticated: () => {
550
+ console.log('Authenticated!');
551
+ // Now safe to subscribe to events and emit messages
552
+ },
553
+ onDisconnected: (reason) => {
554
+ console.log('Disconnected:', reason);
76
555
  },
77
556
  });
78
557
 
79
- socketClient.connect();
558
+ // Connection is established in constructor via configure()
559
+ ```
560
+
561
+ ---
562
+
563
+ ## Methods
564
+
565
+ ### `connect(): void`
566
+
567
+ Explicitly connect to the server (if not already connected).
568
+
569
+ ```typescript
570
+ client.connect();
571
+ ```
572
+
573
+ ### `disconnect(): void`
574
+
575
+ Disconnect from the server.
576
+
577
+ ```typescript
578
+ client.disconnect();
579
+ ```
580
+
581
+ ### `authenticate(): void`
582
+
583
+ Send the `authenticate` event to the server. Only works when connected and in `UNAUTHORIZED` state.
584
+
585
+ ```typescript
586
+ client.authenticate();
587
+ // Server will respond with 'authenticated' or 'unauthenticated' event
588
+ ```
589
+
590
+ ### `getState(): TSocketIOClientState`
591
+
592
+ Returns the current authentication state.
593
+
594
+ ```typescript
595
+ const state = client.getState();
596
+ // 'unauthorized' | 'authenticating' | 'authenticated'
597
+ ```
598
+
599
+ ### `getSocketClient(): Socket`
600
+
601
+ Returns the raw `socket.io-client` `Socket` instance for advanced operations.
602
+
603
+ ```typescript
604
+ const socket = client.getSocketClient();
605
+ socket.on('custom-event', (data) => { /* ... */ });
606
+ ```
607
+
608
+ ### `subscribe<T>(opts): void`
609
+
610
+ Subscribe to a single event with duplicate protection.
611
+
612
+ | Parameter | Type | Default | Description |
613
+ |-----------|------|---------|-------------|
614
+ | `opts.event` | `string` | — | Event name |
615
+ | `opts.handler` | `TSocketIOEventHandler<T>` | — | Event handler (errors are caught internally) |
616
+ | `opts.ignoreDuplicate` | `boolean` | `true` | Skip if handler already exists for this event |
617
+
618
+ ```typescript
619
+ client.subscribe<{ message: string }>({
620
+ event: 'notification',
621
+ handler: (data) => {
622
+ console.log('Got notification:', data.message);
623
+ },
624
+ });
80
625
  ```
81
626
 
82
- ### Subscribing to Events
627
+ ### `subscribeMany(opts): void`
628
+
629
+ Subscribe to multiple events at once.
83
630
 
84
631
  ```typescript
85
- socketClient.subscribe({
632
+ client.subscribeMany({
86
633
  events: {
87
- connect: () => {
88
- console.log('Connected to Socket.IO server!');
89
- // Authenticate with the server
90
- socketClient.emit({ topic: 'authenticate' });
91
- },
92
- authenticated: (data) => {
93
- console.log('Successfully authenticated:', data);
94
- },
95
- notification: (data) => {
96
- console.log('Received notification:', data);
97
- },
98
- disconnect: () => {
99
- console.log('Disconnected from server.');
100
- },
634
+ 'chat:message': (data) => console.log('Chat:', data),
635
+ 'room:update': (data) => console.log('Room:', data),
636
+ 'system:alert': (data) => console.log('Alert:', data),
101
637
  },
102
638
  });
103
639
  ```
104
640
 
641
+ ### `unsubscribe(opts): void`
642
+
643
+ Remove event listener(s).
644
+
645
+ | Parameter | Type | Description |
646
+ |-----------|------|-------------|
647
+ | `opts.event` | `string` | Event name |
648
+ | `opts.handler` | `TSocketIOEventHandler` (optional) | Specific handler to remove. If omitted, removes all handlers |
649
+
650
+ ```typescript
651
+ // Remove specific handler
652
+ client.unsubscribe({ event: 'notification', handler: myHandler });
653
+
654
+ // Remove all handlers for event
655
+ client.unsubscribe({ event: 'notification' });
656
+ ```
657
+
658
+ ### `unsubscribeMany(opts): void`
659
+
660
+ Remove all handlers for multiple events.
661
+
662
+ ```typescript
663
+ client.unsubscribeMany({ events: ['chat:message', 'room:update'] });
664
+ ```
665
+
666
+ ### `emit<T>(opts): void`
667
+
668
+ Emit an event to the server.
669
+
670
+ | Parameter | Type | Required | Description |
671
+ |-----------|------|----------|-------------|
672
+ | `opts.topic` | `string` | Yes | Event name |
673
+ | `opts.data` | `T` | Yes | Event payload |
674
+ | `opts.doLog` | `boolean` | No | Log the emission |
675
+ | `opts.cb` | `() => void` | No | Callback after emit |
676
+
677
+ ```typescript
678
+ client.emit({
679
+ topic: 'chat:message',
680
+ data: { room: 'general', message: 'Hello everyone!' },
681
+ });
682
+ ```
683
+
684
+ > [!NOTE]
685
+ > Throws if the client is not connected. Check `getSocketClient().connected` first if unsure.
686
+
687
+ ### `joinRooms(opts): void`
688
+
689
+ Request to join rooms (emits the `join` event to server).
690
+
691
+ ```typescript
692
+ client.joinRooms({ rooms: ['room-a', 'room-b'] });
693
+ ```
694
+
695
+ ### `leaveRooms(opts): void`
696
+
697
+ Request to leave rooms (emits the `leave` event to server).
698
+
699
+ ```typescript
700
+ client.leaveRooms({ rooms: ['room-a'] });
701
+ ```
702
+
703
+ ### `shutdown(): void`
704
+
705
+ Full cleanup — removes all listeners, disconnects, resets state.
706
+
707
+ ```typescript
708
+ client.shutdown();
709
+ ```
710
+
711
+ ---
712
+
713
+ ## Client Lifecycle
714
+
715
+ ```
716
+ ┌──────────┐
717
+ │ new │ constructor → configure()
718
+ │ Client() │ ├── io(host, options)
719
+ └─────┬─────┘ ├── Register: connect, disconnect, connect_error
720
+ │ ├── Register: authenticated, unauthenticated, ping
721
+ │ └── Connection established (if server is reachable)
722
+
723
+ ┌─────▼─────────┐
724
+ │ Connected │ onConnected callback fires
725
+ │ (UNAUTHORIZED)│
726
+ └─────┬─────────┘
727
+
728
+ │ authenticate()
729
+
730
+ ┌─────▼──────────┐
731
+ │ AUTHENTICATING │ Waiting for server response
732
+ └─────┬──────────┘
733
+
734
+ ┌────┴────┐
735
+ │ │
736
+ ▼ ▼
737
+ ┌────────┐ ┌──────────────┐
738
+ │ AUTH'D │ │ UNAUTH'D │ onUnauthenticated callback
739
+ │ │ │ → disconnect │
740
+ └───┬────┘ └──────────────┘
741
+
742
+ │ onAuthenticated callback
743
+
744
+
745
+ Ready to emit/subscribe
746
+
747
+ │ disconnect() or server disconnect
748
+
749
+ ┌─▼───────────┐
750
+ │ Disconnected │ onDisconnected callback
751
+ │ (UNAUTHORIZED)│ State reset
752
+ └──────────────┘
753
+ ```
754
+
755
+ ---
756
+
757
+ ## Constants Reference
758
+
759
+ ### `SocketIOConstants`
760
+
761
+ | Constant | Value | Description |
762
+ |----------|-------|-------------|
763
+ | `EVENT_PING` | `'ping'` | Server → Client keep-alive (interval configurable via `pingInterval`) |
764
+ | `EVENT_CONNECT` | `'connection'` | Server-side connection event |
765
+ | `EVENT_DISCONNECT` | `'disconnect'` | Disconnection event |
766
+ | `EVENT_JOIN` | `'join'` | Room join request |
767
+ | `EVENT_LEAVE` | `'leave'` | Room leave request |
768
+ | `EVENT_AUTHENTICATE` | `'authenticate'` | Client → Server auth request |
769
+ | `EVENT_AUTHENTICATED` | `'authenticated'` | Server → Client auth success |
770
+ | `EVENT_UNAUTHENTICATE` | `'unauthenticated'` | Server → Client auth failure |
771
+ | `ROOM_DEFAULT` | `'io-default'` | Default room for all authenticated clients |
772
+ | `ROOM_NOTIFICATION` | `'io-notification'` | Default notification room |
773
+
774
+ ### `SocketIOClientStates`
775
+
776
+ | Constant | Value |
777
+ |----------|-------|
778
+ | `UNAUTHORIZED` | `'unauthorized'` |
779
+ | `AUTHENTICATING` | `'authenticating'` |
780
+ | `AUTHENTICATED` | `'authenticated'` |
781
+
782
+ Utility methods:
783
+
784
+ ```typescript
785
+ SocketIOClientStates.isValid('authenticated'); // true
786
+ SocketIOClientStates.isValid('invalid'); // false
787
+ SocketIOClientStates.SCHEME_SET; // Set { 'unauthorized', 'authenticating', 'authenticated' }
788
+ ```
789
+
790
+ ---
791
+
792
+ ## Type Definitions
793
+
794
+ ### `TSocketIOServerOptions`
795
+
796
+ ```typescript
797
+ interface ISocketIOServerBaseOptions {
798
+ identifier: string;
799
+ serverOptions: Partial<ServerOptions>;
800
+ redisConnection: DefaultRedisHelper;
801
+ authenticateFn: TSocketIOAuthenticateFn;
802
+ clientConnectedFn?: TSocketIOClientConnectedFn;
803
+ validateRoomFn?: TSocketIOValidateRoomFn;
804
+ authenticateTimeout?: number;
805
+ pingInterval?: number;
806
+ defaultRooms?: string[];
807
+ }
808
+
809
+ interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
810
+ runtime: typeof RuntimeModules.NODE; // 'node'
811
+ server: HTTPServer;
812
+ }
813
+
814
+ interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
815
+ runtime: typeof RuntimeModules.BUN; // 'bun'
816
+ engine: any; // @socket.io/bun-engine Server instance
817
+ }
818
+
819
+ type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
820
+ ```
821
+
822
+ ### `ISocketIOClient`
823
+
824
+ ```typescript
825
+ interface ISocketIOClient {
826
+ id: string;
827
+ socket: IOSocket;
828
+ state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
829
+ interval?: NodeJS.Timeout; // Ping interval (set after auth)
830
+ authenticateTimeout: NodeJS.Timeout; // Auto-disconnect timer
831
+ }
832
+ ```
833
+
834
+ ### `IHandshake`
835
+
836
+ ```typescript
837
+ interface IHandshake {
838
+ headers: IncomingHttpHeaders;
839
+ time: string;
840
+ address: string;
841
+ xdomain: boolean;
842
+ secure: boolean;
843
+ issued: number;
844
+ url: string;
845
+ query: ParsedUrlQuery;
846
+ auth: { [key: string]: any };
847
+ }
848
+ ```
849
+
850
+ ### `ISocketIOClientOptions`
851
+
852
+ ```typescript
853
+ interface ISocketIOClientOptions {
854
+ identifier: string;
855
+ host: string;
856
+ options: IOptions;
857
+ onConnected?: () => ValueOrPromise<void>;
858
+ onDisconnected?: (reason: string) => ValueOrPromise<void>;
859
+ onError?: (error: Error) => ValueOrPromise<void>;
860
+ onAuthenticated?: () => ValueOrPromise<void>;
861
+ onUnauthenticated?: (message: string) => ValueOrPromise<void>;
862
+ }
863
+ ```
864
+
865
+ ### `TSocketIOAuthenticateFn`
866
+
867
+ ```typescript
868
+ type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
869
+ ```
870
+
871
+ ### `TSocketIOValidateRoomFn`
872
+
873
+ ```typescript
874
+ type TSocketIOValidateRoomFn = (opts: { socket: IOSocket; rooms: string[] }) => ValueOrPromise<string[]>;
875
+ ```
876
+
877
+ ### `TSocketIOClientConnectedFn`
878
+
879
+ ```typescript
880
+ type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;
881
+ ```
882
+
883
+ ### `TSocketIOEventHandler`
884
+
885
+ ```typescript
886
+ type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
887
+ ```
888
+
889
+ ---
890
+
891
+ ## RuntimeModules
892
+
893
+ Used for runtime detection and discriminated union typing:
894
+
895
+ ```typescript
896
+ class RuntimeModules {
897
+ static readonly NODE = 'node';
898
+ static readonly BUN = 'bun';
899
+
900
+ static detect(): TRuntimeModule; // Returns 'bun' or 'node'
901
+ static isBun(): boolean;
902
+ static isNode(): boolean;
903
+ }
904
+
905
+ type TRuntimeModule = 'node' | 'bun'; // TConstValue<typeof RuntimeModules>
906
+ ```
907
+
908
+ ---
909
+
105
910
  ## See Also
106
911
 
107
912
  - **Related Concepts:**
108
- - [Services](/guides/core-concepts/services) - Real-time communication in services
913
+ - [Services](/guides/core-concepts/services) Using helpers in service layer
109
914
 
110
915
  - **Other Helpers:**
111
- - [Helpers Index](./index) - All available helpers
112
- - [Redis Helper](./redis) - For Socket.IO scaling with Redis adapter
916
+ - [Helpers Index](./index) All available helpers
917
+ - [Redis Helper](./redis) `RedisHelper` used for Socket.IO adapter
113
918
 
114
919
  - **References:**
115
- - [Socket.IO Component](/references/components/socket-io) - Full component setup
920
+ - [Socket.IO Component](/references/components/socket-io) Component setup and lifecycle integration
116
921
 
117
922
  - **External Resources:**
118
- - [Socket.IO Documentation](https://socket.io/docs/) - Official Socket.IO docs
119
- - [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/) - Scaling guide
923
+ - [Socket.IO Documentation](https://socket.io/docs/) Official Socket.IO docs
924
+ - [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/) Scaling guide
925
+ - [Socket.IO Client API](https://socket.io/docs/v4/client-api/) — Client reference
926
+ - [@socket.io/bun-engine](https://github.com/socketio/bun-engine) — Bun runtime support
120
927
 
121
928
  - **Tutorials:**
122
- - [Real-Time Chat Application](/guides/tutorials/realtime-chat) - Socket.IO tutorial
929
+ - [Real-Time Chat Application](/guides/tutorials/realtime-chat) Full Socket.IO tutorial
930
+
931
+ - **Changelog:**
932
+ - [2026-02-06: Socket.IO Integration Fix](/changelogs/2026-02-06-socket-io-integration-fix) — Lifecycle timing fix + Bun runtime support