@venizia/ignis-docs 0.0.5 → 0.0.6-1

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.
Files changed (123) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/architectural-patterns.md +0 -2
  3. package/wiki/best-practices/architecture-decisions.md +0 -8
  4. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  5. package/wiki/best-practices/code-style-standards/index.md +0 -1
  6. package/wiki/best-practices/code-style-standards/tooling.md +0 -3
  7. package/wiki/best-practices/contribution-workflow.md +12 -12
  8. package/wiki/best-practices/index.md +4 -14
  9. package/wiki/best-practices/performance-optimization.md +3 -3
  10. package/wiki/best-practices/security-guidelines.md +2 -2
  11. package/wiki/best-practices/troubleshooting-tips.md +1 -1
  12. package/wiki/guides/core-concepts/application/bootstrapping.md +6 -7
  13. package/wiki/guides/core-concepts/components-guide.md +1 -1
  14. package/wiki/guides/core-concepts/components.md +2 -2
  15. package/wiki/guides/core-concepts/dependency-injection.md +4 -5
  16. package/wiki/guides/core-concepts/persistent/datasources.md +4 -5
  17. package/wiki/guides/core-concepts/services.md +1 -1
  18. package/wiki/guides/get-started/5-minute-quickstart.md +4 -5
  19. package/wiki/guides/get-started/philosophy.md +12 -24
  20. package/wiki/guides/index.md +2 -9
  21. package/wiki/guides/reference/mcp-docs-server.md +13 -13
  22. package/wiki/guides/tutorials/building-a-crud-api.md +10 -10
  23. package/wiki/guides/tutorials/complete-installation.md +11 -12
  24. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  25. package/wiki/guides/tutorials/realtime-chat.md +6 -6
  26. package/wiki/guides/tutorials/testing.md +4 -5
  27. package/wiki/index.md +8 -14
  28. package/wiki/references/base/bootstrapping.md +0 -3
  29. package/wiki/references/base/components.md +2 -2
  30. package/wiki/references/base/controllers.md +0 -1
  31. package/wiki/references/base/datasources.md +1 -1
  32. package/wiki/references/base/dependency-injection.md +2 -2
  33. package/wiki/references/base/filter-system/default-filter.md +2 -3
  34. package/wiki/references/base/filter-system/index.md +1 -1
  35. package/wiki/references/base/filter-system/quick-reference.md +0 -14
  36. package/wiki/references/base/middlewares.md +0 -8
  37. package/wiki/references/base/providers.md +0 -9
  38. package/wiki/references/base/repositories/advanced.md +1 -1
  39. package/wiki/references/base/repositories/mixins.md +2 -3
  40. package/wiki/references/base/services.md +0 -1
  41. package/wiki/references/components/authentication/api.md +444 -0
  42. package/wiki/references/components/authentication/errors.md +177 -0
  43. package/wiki/references/components/authentication/index.md +571 -0
  44. package/wiki/references/components/authentication/usage.md +781 -0
  45. package/wiki/references/components/health-check.md +292 -103
  46. package/wiki/references/components/index.md +14 -12
  47. package/wiki/references/components/mail/api.md +505 -0
  48. package/wiki/references/components/mail/errors.md +176 -0
  49. package/wiki/references/components/mail/index.md +535 -0
  50. package/wiki/references/components/mail/usage.md +404 -0
  51. package/wiki/references/components/request-tracker.md +229 -25
  52. package/wiki/references/components/socket-io/api.md +1051 -0
  53. package/wiki/references/components/socket-io/errors.md +119 -0
  54. package/wiki/references/components/socket-io/index.md +410 -0
  55. package/wiki/references/components/socket-io/usage.md +322 -0
  56. package/wiki/references/components/static-asset/api.md +261 -0
  57. package/wiki/references/components/static-asset/errors.md +89 -0
  58. package/wiki/references/components/static-asset/index.md +617 -0
  59. package/wiki/references/components/static-asset/usage.md +364 -0
  60. package/wiki/references/components/swagger.md +390 -110
  61. package/wiki/references/components/template/api-page.md +125 -0
  62. package/wiki/references/components/template/errors-page.md +100 -0
  63. package/wiki/references/components/template/index.md +104 -0
  64. package/wiki/references/components/template/setup-page.md +134 -0
  65. package/wiki/references/components/template/single-page.md +132 -0
  66. package/wiki/references/components/template/usage-page.md +127 -0
  67. package/wiki/references/components/websocket/api.md +508 -0
  68. package/wiki/references/components/websocket/errors.md +123 -0
  69. package/wiki/references/components/websocket/index.md +453 -0
  70. package/wiki/references/components/websocket/usage.md +475 -0
  71. package/wiki/references/helpers/cron/index.md +224 -0
  72. package/wiki/references/helpers/crypto/index.md +537 -0
  73. package/wiki/references/helpers/env/index.md +214 -0
  74. package/wiki/references/helpers/error/index.md +232 -0
  75. package/wiki/references/helpers/index.md +16 -15
  76. package/wiki/references/helpers/inversion/index.md +608 -0
  77. package/wiki/references/helpers/logger/index.md +600 -0
  78. package/wiki/references/helpers/network/api.md +986 -0
  79. package/wiki/references/helpers/network/index.md +620 -0
  80. package/wiki/references/helpers/queue/index.md +589 -0
  81. package/wiki/references/helpers/redis/index.md +495 -0
  82. package/wiki/references/helpers/socket-io/api.md +497 -0
  83. package/wiki/references/helpers/socket-io/index.md +513 -0
  84. package/wiki/references/helpers/storage/api.md +705 -0
  85. package/wiki/references/helpers/storage/index.md +583 -0
  86. package/wiki/references/helpers/template/index.md +66 -0
  87. package/wiki/references/helpers/template/single-page.md +126 -0
  88. package/wiki/references/helpers/testing/index.md +510 -0
  89. package/wiki/references/helpers/types/index.md +512 -0
  90. package/wiki/references/helpers/uid/index.md +272 -0
  91. package/wiki/references/helpers/websocket/api.md +736 -0
  92. package/wiki/references/helpers/websocket/index.md +574 -0
  93. package/wiki/references/helpers/worker-thread/index.md +470 -0
  94. package/wiki/references/index.md +2 -9
  95. package/wiki/references/quick-reference.md +3 -18
  96. package/wiki/references/utilities/jsx.md +1 -8
  97. package/wiki/references/utilities/statuses.md +0 -7
  98. package/wiki/references/components/authentication.md +0 -476
  99. package/wiki/references/components/mail.md +0 -687
  100. package/wiki/references/components/socket-io.md +0 -562
  101. package/wiki/references/components/static-asset.md +0 -1277
  102. package/wiki/references/helpers/cron.md +0 -108
  103. package/wiki/references/helpers/crypto.md +0 -132
  104. package/wiki/references/helpers/env.md +0 -83
  105. package/wiki/references/helpers/error.md +0 -97
  106. package/wiki/references/helpers/inversion.md +0 -176
  107. package/wiki/references/helpers/logger.md +0 -296
  108. package/wiki/references/helpers/network.md +0 -396
  109. package/wiki/references/helpers/queue.md +0 -150
  110. package/wiki/references/helpers/redis.md +0 -142
  111. package/wiki/references/helpers/socket-io.md +0 -932
  112. package/wiki/references/helpers/storage.md +0 -665
  113. package/wiki/references/helpers/testing.md +0 -133
  114. package/wiki/references/helpers/types.md +0 -167
  115. package/wiki/references/helpers/uid.md +0 -167
  116. package/wiki/references/helpers/worker-thread.md +0 -178
  117. package/wiki/references/src-details/boot.md +0 -379
  118. package/wiki/references/src-details/core.md +0 -263
  119. package/wiki/references/src-details/dev-configs.md +0 -298
  120. package/wiki/references/src-details/docs.md +0 -71
  121. package/wiki/references/src-details/helpers.md +0 -211
  122. package/wiki/references/src-details/index.md +0 -86
  123. package/wiki/references/src-details/inversion.md +0 -340
@@ -0,0 +1,1051 @@
1
+ # Socket.IO -- API Reference
2
+
3
+ > Architecture deep dive, method signatures, internals, and type definitions.
4
+
5
+ ## Architecture
6
+
7
+ The component integrates Socket.IO into the Ignis application lifecycle with runtime-specific initialization (Node.js vs Bun).
8
+
9
+ #### Architecture Diagram
10
+ ```
11
+ SocketIOComponent
12
+ +----------------------------------------------+
13
+ | |
14
+ | binding() |
15
+ | |-- resolveBindings() |
16
+ | | |-- SERVER_OPTIONS |
17
+ | | |-- REDIS_CONNECTION |
18
+ | | |-- AUTHENTICATE_HANDLER |
19
+ | | |-- VALIDATE_ROOM_HANDLER |
20
+ | | +-- CLIENT_CONNECTED_HANDLER |
21
+ | | |
22
+ | +-- RuntimeModules.detect() |
23
+ | |-- BUN -> registerBunHook() |
24
+ | +-- NODE -> registerNodeHook() |
25
+ | |
26
+ | (Post-start hooks execute after server) |
27
+ | |-- Creates SocketIOServerHelper |
28
+ | |-- await socketIOHelper.configure() |
29
+ | |-- Binds to SOCKET_IO_INSTANCE |
30
+ | +-- Wires into server (runtime-specific) |
31
+ +----------------------------------------------+
32
+ ```
33
+
34
+ ### Lifecycle Integration
35
+
36
+ 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.
37
+
38
+ #### Application Lifecycle Diagram
39
+ ```
40
+ Application Lifecycle
41
+ =====================
42
+
43
+ +------------------+
44
+ | preConfigure() | <-- Register SocketIOComponent here
45
+ +--------+---------+
46
+ |
47
+ +--------v---------+
48
+ | initialize() | <-- Component.binding() runs here
49
+ | | Resolves bindings, registers post-start hook
50
+ +--------+---------+
51
+ |
52
+ +--------v---------+
53
+ | setupMiddlewares |
54
+ +--------+---------+
55
+ |
56
+ +--------v-----------------------+
57
+ | startBunModule() OR | <-- Server starts, instance created
58
+ | startNodeModule() |
59
+ +--------+-----------------------+
60
+ |
61
+ +--------v--------------------------+
62
+ | executePostStartHooks() | <-- SocketIOServerHelper created HERE
63
+ | +-- socket-io-initialize | Server instance is now available
64
+ +-----------------------------------+
65
+ ```
66
+
67
+ ### Runtime-Specific Behavior
68
+
69
+ | Aspect | Node.js | Bun |
70
+ |--------|---------|-----|
71
+ | **Server Type** | `node:http.Server` | `Bun.Server` |
72
+ | **IO Server Init** | `new IOServer(httpServer, opts)` | `new IOServer()` + `io.bind(engine)` |
73
+ | **Engine** | Built-in (`socket.io`) | `@socket.io/bun-engine` (optional peer dep) |
74
+ | **Request Routing** | Socket.IO attaches to HTTP server automatically | `server.reload({ fetch, websocket })` wires engine into Bun's request loop |
75
+ | **WebSocket Upgrade** | Handled by `node:http.Server` upgrade event | Handled by Bun's `websocket` handler |
76
+ | **Dynamic Import** | None needed | `await import('@socket.io/bun-engine')` at runtime |
77
+ | **Fetch Handler** | Not needed -- HTTP server handles upgrades | Custom fetch wraps Hono fetch, routes WS upgrades to engine |
78
+ | **CORS** | Handled by `socket.io` CORS options | Handled by Bun engine options (requires explicit field bridging) |
79
+ | **Server Access** | Direct -- Socket.IO attaches to HTTP server | `server.reload({ fetch, websocket })` to hot-swap handlers |
80
+
81
+ ### Runtime Differences -- Deep Dive
82
+
83
+ #### Bun Runtime
84
+
85
+ The Bun handler creates a custom fetch function that intercepts WebSocket upgrade requests:
86
+
87
+ 1. Checks if the request path matches the Socket.IO path (`serverOptions.path`, default `'/io'`)
88
+ 2. If yes, delegates to `@socket.io/bun-engine` via `engine.handleRequest(req, server)` for WebSocket protocol handling
89
+ 3. If no, delegates to Hono's normal `server.fetch(req, server)` handler
90
+
91
+ #### Bun Fetch Handler Source
92
+ ```typescript
93
+ function createBunFetchHandler(opts: {
94
+ engine: any;
95
+ enginePath: string;
96
+ honoServer: OpenAPIHono;
97
+ }): (req: Request, server: TBunServerInstance) => Response | Promise<Response> {
98
+ const { engine, enginePath, honoServer } = opts;
99
+
100
+ return (req: Request, server: TBunServerInstance): Response | Promise<Response> => {
101
+ const url = new URL(req.url);
102
+
103
+ if (!url.pathname.startsWith(enginePath)) {
104
+ return honoServer.fetch(req, server);
105
+ }
106
+
107
+ return engine.handleRequest(req, server) ?? new Response(null, { status: 404 });
108
+ };
109
+ }
110
+ ```
111
+
112
+ **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`:
113
+
114
+ #### Bun Engine CORS Bridging
115
+ ```typescript
116
+ const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
117
+ const engine = new BunEngine({
118
+ path: serverOptions.path ?? '/socket.io/',
119
+ ...(corsConfig && {
120
+ cors: {
121
+ origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
122
+ methods: corsConfig.methods,
123
+ credentials: corsConfig.credentials,
124
+ allowedHeaders: corsConfig.allowedHeaders,
125
+ exposedHeaders: corsConfig.exposedHeaders,
126
+ maxAge: corsConfig.maxAge,
127
+ },
128
+ }),
129
+ });
130
+ ```
131
+
132
+ #### Node.js Runtime
133
+
134
+ 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:
135
+
136
+ #### Node.js Handler Source
137
+ ```typescript
138
+ async function createNodeSocketIOHelper(opts: {
139
+ serverOptions: Partial<IServerOptions>;
140
+ httpServer: TNodeServerInstance;
141
+ resolvedBindings: IResolvedBindings;
142
+ }): Promise<SocketIOServerHelper> {
143
+ const { serverOptions, httpServer, resolvedBindings } = opts;
144
+ const { redisConnection, authenticateFn, validateRoomFn, clientConnectedFn } = resolvedBindings;
145
+
146
+ const socketIOHelper = new SocketIOServerHelper({
147
+ runtime: RuntimeModules.NODE,
148
+ identifier: serverOptions.identifier!,
149
+ server: httpServer,
150
+ serverOptions,
151
+ redisConnection,
152
+ authenticateFn,
153
+ validateRoomFn,
154
+ clientConnectedFn,
155
+ });
156
+ await socketIOHelper.configure();
157
+
158
+ return socketIOHelper;
159
+ }
160
+ ```
161
+
162
+ ## Server Helper API Reference
163
+
164
+ ### `SocketIOServerHelper` Constructor
165
+
166
+ The helper uses a **discriminated union** for its constructor options, keyed on `runtime`:
167
+
168
+ #### `TSocketIOServerOptions` Type
169
+ ```typescript
170
+ interface ISocketIOServerBaseOptions {
171
+ identifier: string;
172
+ serverOptions: Partial<ServerOptions>;
173
+ redisConnection: DefaultRedisHelper;
174
+ defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
175
+ authenticateTimeout?: number; // Default: 10_000 (10 seconds)
176
+ pingInterval?: number; // Default: 30_000 (30 seconds)
177
+
178
+ authenticateFn: TSocketIOAuthenticateFn;
179
+ validateRoomFn?: TSocketIOValidateRoomFn;
180
+ clientConnectedFn?: TSocketIOClientConnectedFn;
181
+ }
182
+
183
+ interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
184
+ runtime: typeof RuntimeModules.NODE;
185
+ server: HTTPServer; // node:http.Server instance
186
+ }
187
+
188
+ interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
189
+ runtime: typeof RuntimeModules.BUN;
190
+ engine: any; // @socket.io/bun-engine Server instance
191
+ }
192
+
193
+ type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
194
+ ```
195
+
196
+ During construction:
197
+
198
+ 1. Sets `identifier`, `runtime`, `serverOptions`, callback functions
199
+ 2. Sets defaults: `authenticateTimeout` = 10s, `pingInterval` = 30s, `defaultRooms` = `['io-default', 'io-notification']`
200
+ 3. Calls `setRuntime()` -- validates and stores the server or engine
201
+ 4. Calls `initRedisClients()` -- creates 3 duplicated Redis clients from the connection
202
+
203
+ > [!IMPORTANT]
204
+ > 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.
205
+
206
+ ### `configure()` -- Server Initialization
207
+
208
+ The `configure()` method is the main initialization entry point, called after construction:
209
+
210
+ ```
211
+ configure()
212
+ |-- Register error handlers on all 3 Redis clients
213
+ |-- Connect any clients in 'wait' status (lazyConnect mode)
214
+ |-- await Promise.all([redisPub.ready, redisSub.ready, redisEmitter.ready])
215
+ |-- initIOServer()
216
+ | |-- NODE: new IOServer(httpServer, serverOptions)
217
+ | +-- BUN: new IOServer() -> io.bind(bunEngine)
218
+ |-- io.adapter(createAdapter(redisPub, redisSub))
219
+ |-- emitter = new Emitter(redisEmitter)
220
+ +-- io.on('connection', onClientConnect)
221
+ ```
222
+
223
+ > [!NOTE]
224
+ > 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.
225
+
226
+ ### Public Methods
227
+
228
+ #### `getIOServer()`
229
+
230
+ ```typescript
231
+ getIOServer(): IOServer
232
+ ```
233
+
234
+ Returns 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()`).
235
+
236
+ #### `getEngine()`
237
+
238
+ ```typescript
239
+ getEngine(): any
240
+ ```
241
+
242
+ Returns the `@socket.io/bun-engine` instance. **Throws** if the runtime is Node.js (`"Engine is only available for Bun runtime!"`).
243
+
244
+ #### `getClients()`
245
+
246
+ ```typescript
247
+ // Overloaded:
248
+ getClients(): Map<string, ISocketIOClient>
249
+ getClients(opts: { id: string }): ISocketIOClient | undefined
250
+ ```
251
+
252
+ When called without arguments, returns the full client map. When called with `{ id }`, returns the specific client entry or `undefined` if not found.
253
+
254
+ #### `on()`
255
+
256
+ ```typescript
257
+ on<HandlerArgsType extends unknown[] = unknown[], HandlerReturnType = void>(opts: {
258
+ topic: string;
259
+ handler: (...args: HandlerArgsType) => ValueOrPromise<HandlerReturnType>;
260
+ }): void
261
+ ```
262
+
263
+ Registers a server-level event handler on the IO server. **Throws** if `topic` is empty, `handler` is falsy, or the IO server is not initialized.
264
+
265
+ #### `ping()`
266
+
267
+ ```typescript
268
+ ping(opts: { socket: IOSocket; doIgnoreAuth: boolean }): void
269
+ ```
270
+
271
+ Sends a `ping` event to a specific client with `{ time: <ISO string> }`. Behavior:
272
+
273
+ - If `socket` is undefined, logs and returns
274
+ - If client is not found in the client map, logs and returns
275
+ - If `doIgnoreAuth` is `false` and the client is not `authenticated`, disconnects the client
276
+ - If `doIgnoreAuth` is `true`, sends the ping regardless of auth state
277
+
278
+ 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.
279
+
280
+ #### `disconnect()`
281
+
282
+ ```typescript
283
+ disconnect(opts: { socket: IOSocket }): void
284
+ ```
285
+
286
+ Disconnects a specific client and cleans up resources:
287
+
288
+ 1. Clears the ping interval (if set)
289
+ 2. Clears the authentication timeout
290
+ 3. Removes the client from the `clients` map
291
+ 4. Calls `socket.disconnect()` on the underlying Socket.IO socket
292
+
293
+ If the socket is `undefined` or not tracked in the client map, the method still calls `socket.disconnect()` for safety.
294
+
295
+ #### `onClientConnect()`
296
+
297
+ ```typescript
298
+ onClientConnect(opts: { socket: IOSocket }): void
299
+ ```
300
+
301
+ Handles 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.
302
+
303
+ Behavior:
304
+ 1. Validates the socket exists (returns if `null`/`undefined`)
305
+ 2. Checks for duplicate connections by socket ID (returns if already tracked)
306
+ 3. Creates an `ISocketIOClient` entry with state `UNAUTHORIZED`
307
+ 4. Starts the authentication timeout (`authenticateTimeout` ms)
308
+ 5. Registers `disconnect` handler on the socket
309
+ 6. Registers `authenticate` handler via `registerAuthHandler()`
310
+
311
+ #### `onClientAuthenticated()`
312
+
313
+ ```typescript
314
+ onClientAuthenticated(opts: { socket: IOSocket }): void
315
+ ```
316
+
317
+ Called after successful authentication. This method is public so it can be invoked externally for testing or custom auth flows.
318
+
319
+ Behavior:
320
+ 1. Validates the socket and client entry exist
321
+ 2. Sets client state to `AUTHENTICATED`
322
+ 3. Sends an initial ping
323
+ 4. Joins default rooms (`io-default`, `io-notification`)
324
+ 5. Registers room handlers (`join`, `leave`)
325
+ 6. Starts the ping interval
326
+ 7. Emits `authenticated` event to the client with `{ id, time }`
327
+ 8. Invokes the `clientConnectedFn` callback (if configured)
328
+
329
+ ### Messaging via `send()`
330
+
331
+ The `send()` method uses the Redis emitter for message delivery, enabling cross-instance broadcasting:
332
+
333
+ ```typescript
334
+ send(opts: {
335
+ destination?: string; // Socket ID, room name, or omit for broadcast
336
+ payload: {
337
+ topic: string; // Event name
338
+ data: any; // Event payload
339
+ };
340
+ doLog?: boolean; // Log the emission (default: false)
341
+ cb?: () => void; // Callback executed via setImmediate after emit
342
+ })
343
+ ```
344
+
345
+ Key behaviors:
346
+
347
+ - All messages are **compressed** via `emitter.compress(true)`
348
+ - If `destination` is provided and non-empty, sends via `sender.to(destination).emit(topic, data)`
349
+ - If `destination` is omitted/empty, broadcasts to **all** connected clients via `sender.emit(topic, data)`
350
+ - Callback (`cb`) is executed asynchronously via `setImmediate()`, not after delivery confirmation
351
+ - Logging is opt-in (`doLog: true`) to avoid noise in high-throughput scenarios
352
+
353
+ #### `send()` Silent Failure Behavior
354
+
355
+ The `send()` method silently returns (no error, no log) in these cases:
356
+ - `payload` is falsy
357
+ - `payload.topic` is falsy
358
+ - `payload.data` is falsy
359
+
360
+ 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.
361
+
362
+ > [!TIP]
363
+ > 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.
364
+
365
+ ### Shutdown
366
+
367
+ ```typescript
368
+ shutdown(): Promise<void>
369
+ ```
370
+
371
+ Gracefully shuts down the server:
372
+
373
+ 1. Iterates all tracked clients and clears their intervals/timeouts
374
+ 2. Disconnects each client socket
375
+ 3. Clears the client map
376
+ 4. Closes the IO server (async, wrapped in a Promise)
377
+ 5. Quits all 3 Redis connections (`redisPub`, `redisSub`, `redisEmitter`)
378
+
379
+ ## Client Helper API Reference
380
+
381
+ `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.
382
+
383
+ ### Constructor
384
+
385
+ ```typescript
386
+ constructor(opts: ISocketIOClientOptions)
387
+ ```
388
+
389
+ #### `ISocketIOClientOptions` Interface
390
+
391
+ ```typescript
392
+ interface ISocketIOClientOptions {
393
+ identifier: string;
394
+ host: string;
395
+ options: IOptions;
396
+
397
+ // Lifecycle callbacks (all optional)
398
+ onConnected?: () => ValueOrPromise<void>;
399
+ onDisconnected?: (reason: string) => ValueOrPromise<void>;
400
+ onError?: (error: Error) => ValueOrPromise<void>;
401
+ onAuthenticated?: () => ValueOrPromise<void>;
402
+ onUnauthenticated?: (message: string) => ValueOrPromise<void>;
403
+ }
404
+ ```
405
+
406
+ #### `IOptions` Interface
407
+
408
+ ```typescript
409
+ interface IOptions extends SocketOptions {
410
+ path: string;
411
+ extraHeaders: Record<string | symbol | number, any>;
412
+ }
413
+ ```
414
+
415
+ `IOptions` extends `SocketOptions` from `socket.io-client` with two required fields:
416
+ - `path` -- the Socket.IO endpoint path (must match the server's `path` option, e.g., `'/io'`)
417
+ - `extraHeaders` -- headers sent with every request, commonly used for `authorization` tokens
418
+
419
+ #### Constructor Behavior
420
+
421
+ 1. Calls `super({ scope: opts.identifier })` to initialize `BaseHelper` with scoped logging
422
+ 2. Stores the `identifier`, `host`, `options`, and all lifecycle callbacks
423
+ 3. Immediately calls `configure()` to create the socket and register internal handlers
424
+
425
+ ### `configure()`
426
+
427
+ ```typescript
428
+ configure(): void
429
+ ```
430
+
431
+ Creates 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.
432
+
433
+ Registered handlers:
434
+
435
+ | Event | Internal Behavior |
436
+ |-------|-------------------|
437
+ | `connect` | Logs connection, invokes `onConnected` callback |
438
+ | `disconnect` | Logs disconnection with reason, resets state to `unauthorized`, invokes `onDisconnected` callback |
439
+ | `connect_error` | Logs the error, invokes `onError` callback |
440
+ | `authenticated` | Logs auth data, sets state to `authenticated`, invokes `onAuthenticated` callback |
441
+ | `unauthenticated` | Logs warning with auth data, resets state to `unauthorized`, invokes `onUnauthenticated` callback with the message |
442
+ | `ping` | Logs debug-level ping received |
443
+
444
+ All lifecycle callbacks are wrapped in `Promise.resolve(...).catch(...)` to prevent callback errors from crashing the client.
445
+
446
+ ### `getState()`
447
+
448
+ ```typescript
449
+ getState(): TSocketIOClientState
450
+ ```
451
+
452
+ Returns the current authentication state: `'unauthorized'`, `'authenticating'`, or `'authenticated'`.
453
+
454
+ #### `TSocketIOClientState` Type
455
+
456
+ ```typescript
457
+ type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
458
+ // Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
459
+ ```
460
+
461
+ ### `getSocketClient()`
462
+
463
+ ```typescript
464
+ getSocketClient(): Socket
465
+ ```
466
+
467
+ Returns 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`).
468
+
469
+ ### `authenticate()`
470
+
471
+ ```typescript
472
+ authenticate(): void
473
+ ```
474
+
475
+ Initiates 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`.
476
+
477
+ Guard conditions (no-op with warning log):
478
+ - Socket is not connected (`!this.client?.connected`)
479
+ - Current state is not `unauthorized` (prevents double-auth or re-auth while authenticating)
480
+
481
+ On call:
482
+ 1. Sets state to `authenticating`
483
+ 2. Emits `SocketIOConstants.EVENT_AUTHENTICATE` (value: `'authenticate'`)
484
+
485
+ ### `subscribe()`
486
+
487
+ ```typescript
488
+ subscribe<T = unknown>(opts: {
489
+ event: string;
490
+ handler: TSocketIOEventHandler<T>;
491
+ ignoreDuplicate?: boolean; // default: true
492
+ }): void
493
+ ```
494
+
495
+ Subscribes to a Socket.IO event with automatic error safety.
496
+
497
+ Guard conditions (no-op with warning log):
498
+ - `handler` is falsy
499
+ - `ignoreDuplicate` is `true` (default) and the event already has listeners
500
+
501
+ #### Handler Wrapping Pattern
502
+
503
+ Handlers are wrapped in a **dual try-catch** that catches both synchronous throws and asynchronous rejections:
504
+
505
+ ```typescript
506
+ const wrappedHandler = (data: T) => {
507
+ try {
508
+ Promise.resolve(handler(data)).catch(error => {
509
+ logger.error('Handler error | event: %s | error: %s', event, error);
510
+ });
511
+ } catch (error) {
512
+ logger.error('Handler error | event: %s | error: %s', event, error);
513
+ }
514
+ };
515
+ ```
516
+
517
+ 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.
518
+
519
+ #### `TSocketIOEventHandler<T>` Type
520
+
521
+ ```typescript
522
+ type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
523
+ ```
524
+
525
+ Handlers can be synchronous (`void`) or asynchronous (`Promise<void>`). Both are handled correctly by the wrapping pattern.
526
+
527
+ ### `subscribeMany()`
528
+
529
+ ```typescript
530
+ subscribeMany(opts: {
531
+ events: Record<string, TSocketIOEventHandler>;
532
+ ignoreDuplicate?: boolean;
533
+ }): void
534
+ ```
535
+
536
+ Batch subscribes to multiple events. Iterates over the `events` record and calls `subscribe()` for each entry.
537
+
538
+ ### `unsubscribe()`
539
+
540
+ ```typescript
541
+ unsubscribe(opts: { event: string; handler?: TSocketIOEventHandler }): void
542
+ ```
543
+
544
+ Removes 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)`.
545
+
546
+ No-op if the socket has no listeners for the event.
547
+
548
+ ### `unsubscribeMany()`
549
+
550
+ ```typescript
551
+ unsubscribeMany(opts: { events: string[] }): void
552
+ ```
553
+
554
+ Removes all handlers for each event in the array. Calls `unsubscribe({ event })` for each entry.
555
+
556
+ ### `connect()`
557
+
558
+ ```typescript
559
+ connect(): void
560
+ ```
561
+
562
+ Manually connects the socket. No-op with an info log if the client is not initialized. Useful when `autoConnect: false` is set in the options.
563
+
564
+ ### `disconnect()`
565
+
566
+ ```typescript
567
+ disconnect(): void
568
+ ```
569
+
570
+ Manually disconnects the socket. No-op with an info log if the client is not initialized.
571
+
572
+ ### `emit()`
573
+
574
+ ```typescript
575
+ emit<T = unknown>(opts: {
576
+ topic: string;
577
+ data: T;
578
+ doLog?: boolean; // default: false
579
+ cb?: () => void;
580
+ }): void
581
+ ```
582
+
583
+ Emits an event to the server.
584
+
585
+ **Throws** (via `getError()`) if:
586
+ - The socket is not connected (`statusCode: 400`, message: `"Invalid socket client state to emit"`)
587
+ - The `topic` is falsy (`statusCode: 400`, message: `"Topic is required to emit"`)
588
+
589
+ If `cb` is provided, it is executed via `setImmediate()` (asynchronously, not after server acknowledgment). If `doLog` is `true`, logs the topic and data.
590
+
591
+ ### `joinRooms()`
592
+
593
+ ```typescript
594
+ joinRooms(opts: { rooms: string[] }): void
595
+ ```
596
+
597
+ Emits a `join` event to the server with `{ rooms }`. The server will validate via `validateRoomFn` and perform the actual join.
598
+
599
+ No-op with warning log if the socket is not connected.
600
+
601
+ ### `leaveRooms()`
602
+
603
+ ```typescript
604
+ leaveRooms(opts: { rooms: string[] }): void
605
+ ```
606
+
607
+ Emits a `leave` event to the server with `{ rooms }`. The server performs the actual leave without validation.
608
+
609
+ No-op with warning log if the socket is not connected.
610
+
611
+ ### `shutdown()`
612
+
613
+ ```typescript
614
+ shutdown(): void
615
+ ```
616
+
617
+ Clean shutdown of the client:
618
+
619
+ 1. Calls `removeAllListeners()` on the underlying socket to prevent memory leaks
620
+ 2. Disconnects if still connected
621
+ 3. Resets state to `unauthorized`
622
+
623
+ ## Internals
624
+
625
+ ### `resolveBindings()`
626
+
627
+ Reads all binding keys from the DI container and validates required ones:
628
+
629
+ | Binding | Validation | Error on Failure |
630
+ |---------|-----------|------------------|
631
+ | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
632
+ | `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
633
+ | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"Invalid authenticateFn to setup io socket server!"` |
634
+ | `VALIDATE_ROOM_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
635
+ | `CLIENT_CONNECTED_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
636
+
637
+ ### `registerBunHook()`
638
+
639
+ Registers a post-start hook that:
640
+
641
+ 1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
642
+ 2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`
643
+ 3. Awaits `socketIOHelper.configure()` which waits for all Redis connections to be ready before initializing the adapter and emitter
644
+ 4. Binds the helper to `SOCKET_IO_INSTANCE`
645
+ 5. Gets the Bun server instance and Hono server, then calls `serverInstance.reload()` to wire the engine's `fetch` and `websocket` handlers into the running Bun server
646
+
647
+ ### `createBunEngine()` Function
648
+
649
+ ```typescript
650
+ async function createBunEngine(opts: {
651
+ serverOptions: Partial<ServerOptions>;
652
+ }): Promise<{ engine: any; engineHandler: any }>
653
+ ```
654
+
655
+ 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.
656
+
657
+ ### `createBunFetchHandler()` Function
658
+
659
+ ```typescript
660
+ function createBunFetchHandler(opts: {
661
+ engine: any;
662
+ enginePath: string;
663
+ honoServer: OpenAPIHono;
664
+ }): (req: Request, server: TBunServerInstance) => Response | Promise<Response>
665
+ ```
666
+
667
+ Returns a fetch handler function that routes requests:
668
+ - If `url.pathname` starts with `enginePath`, delegates to `engine.handleRequest(req, server)` (returns 404 Response if `handleRequest` returns nullish)
669
+ - Otherwise, delegates to `honoServer.fetch(req, server)` for normal Hono routing
670
+
671
+ ### `registerNodeHook()`
672
+
673
+ Registers a post-start hook that:
674
+
675
+ 1. Gets the HTTP server instance via `getServerInstance()`
676
+ 2. Validates the server instance exists (throws `"HTTP server not available for Node.js runtime!"` if not)
677
+ 3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
678
+ 4. Binds the helper to `SOCKET_IO_INSTANCE`
679
+
680
+ Node mode is simpler because Socket.IO natively attaches to `node:http.Server`.
681
+
682
+ ### Redis 3-Client Architecture
683
+
684
+ The server helper creates 3 independent Redis connections from a single `DefaultRedisHelper`:
685
+
686
+ ```
687
+ RedisHelper (parent -- NOT consumed)
688
+ |
689
+ +-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
690
+ |
691
+ +-- client.duplicate() --> redisSub (for Redis adapter -- subscribes)
692
+ |
693
+ +-- client.duplicate() --> redisEmitter (for @socket.io/redis-emitter -- message delivery)
694
+ ```
695
+
696
+ **Why 3 clients?**
697
+ - `@socket.io/redis-adapter` requires separate pub and sub clients because a Redis connection in subscribe mode cannot execute other commands
698
+ - `@socket.io/redis-emitter` uses its own client to emit messages independently of the adapter, enabling cross-instance broadcasting even from contexts without a direct Socket.IO reference
699
+ - The parent `RedisHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
700
+
701
+ **`TRedisClient` type:**
702
+ ```typescript
703
+ type TRedisClient = Redis | Cluster;
704
+ ```
705
+
706
+ This supports both single-instance `Redis` and `Cluster` connections from ioredis, making the helper transparent to the Redis deployment topology.
707
+
708
+ ### `setRuntime()` -- Runtime Validation
709
+
710
+ The private `setRuntime()` method validates the constructor options based on the `runtime` discriminant:
711
+
712
+ | Runtime | Required Field | Error on Missing |
713
+ |---------|---------------|------------------|
714
+ | `RuntimeModules.NODE` | `opts.server` (HTTPServer) | `"Invalid HTTP server for Node.js runtime!"` |
715
+ | `RuntimeModules.BUN` | `opts.engine` (BunEngine) | `"Invalid @socket.io/bun-engine instance for Bun runtime!"` |
716
+ | Other | -- | `"Unsupported runtime!"` |
717
+
718
+ ### `initRedisClients()` -- Redis Initialization
719
+
720
+ ```typescript
721
+ private initRedisClients(redisConnection: TSocketIOServerOptions['redisConnection']): void
722
+ ```
723
+
724
+ **Throws** if `redisConnection` is falsy: `"Invalid redis connection to config socket.io adapter!"`
725
+
726
+ Creates 3 duplicated clients from the parent connection's underlying ioredis client.
727
+
728
+ ### `initIOServer()` -- IO Server Initialization
729
+
730
+ Called during `configure()` after Redis connections are ready:
731
+
732
+ | Runtime | Initialization |
733
+ |---------|---------------|
734
+ | `RuntimeModules.NODE` | `this.io = new IOServer(this.server, this.serverOptions)` |
735
+ | `RuntimeModules.BUN` | `this.io = new IOServer()` then `this.io.bind(this.bunEngine)` |
736
+ | Other | Throws `"Unsupported runtime: <runtime>"` |
737
+
738
+ Additional validation errors:
739
+ - Node.js without `this.server`: `"[DANGER] Invalid HTTP server instance to init Socket.io server!"`
740
+ - Bun without `this.bunEngine`: `"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"`
741
+
742
+ ### Connection Lifecycle
743
+
744
+ When a client connects, the server manages a strict authentication flow:
745
+
746
+ ```
747
+ Client connects
748
+ |
749
+ +-- onClientConnect({ socket })
750
+ | +-- Validate socket exists and not duplicate
751
+ | +-- Create ISocketIOClient entry (state: UNAUTHORIZED)
752
+ | +-- Start authenticateTimeout (10s default)
753
+ | +-- Register 'disconnect' handler
754
+ | +-- Register 'authenticate' handler
755
+ |
756
+ +-- Client emits 'authenticate'
757
+ | +-- Validate client exists and state is UNAUTHORIZED
758
+ | +-- Set state to AUTHENTICATING
759
+ | +-- Call authenticateFn(handshake)
760
+ | +-- Success -> onClientAuthenticated()
761
+ | | +-- Set state to AUTHENTICATED
762
+ | | +-- Send initial ping
763
+ | | +-- Join default rooms (io-default, io-notification)
764
+ | | +-- Register 'join' and 'leave' room handlers
765
+ | | +-- Start ping interval (30s default)
766
+ | | +-- Emit 'authenticated' with { id, time }
767
+ | | +-- Call clientConnectedFn({ socket }) if provided
768
+ | +-- Failure -> emit 'unauthenticated' -> disconnect
769
+ |
770
+ +-- Timeout (10s) -> disconnect if not AUTHENTICATED
771
+ ```
772
+
773
+ #### Authentication Failure -- Two Code Paths
774
+
775
+ The `registerAuthHandler()` method handles authentication results through two distinct code paths:
776
+
777
+ **Path 1: `authenticateFn` returns `false`** (`.then()` handler):
778
+ - Sets client state back to `UNAUTHORIZED`
779
+ - Sends `unauthenticated` event with message: `"Invalid token to authenticate! Please login again!"`
780
+ - Disconnects after send via `setImmediate` callback
781
+ - No error logging (this is an expected outcome)
782
+
783
+ **Path 2: `authenticateFn` throws an error** (`.catch()` handler):
784
+ - Sets client state back to `UNAUTHORIZED`
785
+ - Logs the error at error level
786
+ - Sends `unauthenticated` event with message: `"Failed to authenticate connection! Please login again!"`
787
+ - Sets `doLog: true` on the send call (unlike Path 1)
788
+ - Disconnects after send via `setImmediate` callback
789
+
790
+ Both paths also handle the edge case where the client disconnected *during* authentication -- they check `this.clients.has(id)` before proceeding.
791
+
792
+ #### `ISocketIOClient` Interface
793
+ ```typescript
794
+ interface ISocketIOClient {
795
+ id: string;
796
+ socket: IOSocket;
797
+ state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
798
+ interval?: NodeJS.Timeout; // Ping interval (set after auth)
799
+ authenticateTimeout: NodeJS.Timeout; // Auth deadline (cleared on success)
800
+ }
801
+ ```
802
+
803
+ ### Room Handlers
804
+
805
+ Room join/leave handlers are registered after successful authentication:
806
+
807
+ - **`join`**: Client emits `{ rooms: string[] }`. If `validateRoomFn` is configured, only the rooms it returns are joined. If `validateRoomFn` is **not** configured, join is silently rejected with a warning log.
808
+ - **`leave`**: Client emits `{ rooms: string[] }`. Leave is always allowed -- no validation function needed.
809
+
810
+ Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. Empty `rooms` arrays are silently ignored.
811
+
812
+ Join handler validation errors are caught and logged but do not disconnect the client.
813
+
814
+ > [!WARNING]
815
+ > 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.
816
+
817
+ ## Types Reference
818
+
819
+ ### Server Types
820
+
821
+ ```typescript
822
+ // Server constructor options -- discriminated union on 'runtime'
823
+ type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
824
+
825
+ // Base options shared by both runtimes
826
+ interface ISocketIOServerBaseOptions {
827
+ identifier: string;
828
+ serverOptions: Partial<ServerOptions>;
829
+ redisConnection: DefaultRedisHelper;
830
+ defaultRooms?: string[];
831
+ authenticateTimeout?: number;
832
+ pingInterval?: number;
833
+ authenticateFn: TSocketIOAuthenticateFn;
834
+ validateRoomFn?: TSocketIOValidateRoomFn;
835
+ clientConnectedFn?: TSocketIOClientConnectedFn;
836
+ }
837
+
838
+ // Node.js runtime variant
839
+ interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
840
+ runtime: typeof RuntimeModules.NODE;
841
+ server: HTTPServer;
842
+ }
843
+
844
+ // Bun runtime variant
845
+ interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
846
+ runtime: typeof RuntimeModules.BUN;
847
+ engine: any;
848
+ }
849
+
850
+ // Tracked client entry (server-side)
851
+ interface ISocketIOClient {
852
+ id: string;
853
+ socket: IOSocket;
854
+ state: TSocketIOClientState;
855
+ interval?: NodeJS.Timeout;
856
+ authenticateTimeout: NodeJS.Timeout;
857
+ }
858
+
859
+ // Redis client type alias
860
+ type TRedisClient = Redis | Cluster;
861
+ ```
862
+
863
+ ### Client Types
864
+
865
+ ```typescript
866
+ // Client constructor options
867
+ interface ISocketIOClientOptions {
868
+ identifier: string;
869
+ host: string;
870
+ options: IOptions;
871
+ onConnected?: () => ValueOrPromise<void>;
872
+ onDisconnected?: (reason: string) => ValueOrPromise<void>;
873
+ onError?: (error: Error) => ValueOrPromise<void>;
874
+ onAuthenticated?: () => ValueOrPromise<void>;
875
+ onUnauthenticated?: (message: string) => ValueOrPromise<void>;
876
+ }
877
+
878
+ // Socket connection options (extends socket.io-client's SocketOptions)
879
+ interface IOptions extends SocketOptions {
880
+ path: string;
881
+ extraHeaders: Record<string | symbol | number, any>;
882
+ }
883
+
884
+ // Event handler type (supports sync and async)
885
+ type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
886
+
887
+ // Client state type
888
+ type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
889
+ // Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
890
+ ```
891
+
892
+ ### Callback Types
893
+
894
+ ```typescript
895
+ // Server authentication handler
896
+ type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
897
+
898
+ // Server room validation handler
899
+ type TSocketIOValidateRoomFn = (opts: {
900
+ socket: IOSocket;
901
+ rooms: string[];
902
+ }) => ValueOrPromise<string[]>;
903
+
904
+ // Server client connected handler
905
+ type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;
906
+ ```
907
+
908
+ ### Component Types
909
+
910
+ ```typescript
911
+ // Extended ServerOptions with identifier
912
+ interface IServerOptions extends ServerOptions {
913
+ identifier: string;
914
+ }
915
+
916
+ // Resolved binding values from DI container
917
+ interface IResolvedBindings {
918
+ redisConnection: DefaultRedisHelper;
919
+ authenticateFn: TSocketIOAuthenticateFn;
920
+ validateRoomFn?: TSocketIOValidateRoomFn;
921
+ clientConnectedFn?: TSocketIOClientConnectedFn;
922
+ }
923
+ ```
924
+
925
+ ## Post-Start Hook System
926
+
927
+ 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.
928
+
929
+ The component relies on `AbstractApplication`'s post-start hook system:
930
+
931
+ #### API
932
+
933
+ ```typescript
934
+ // Register a hook (during binding phase)
935
+ application.registerPostStartHook({
936
+ identifier: string, // Unique name for logging
937
+ hook: () => ValueOrPromise<void>, // Async function to execute
938
+ });
939
+
940
+ // Get the server instance (available after start)
941
+ application.getServerInstance<T>(): T | undefined;
942
+ ```
943
+
944
+ #### Hook Execution Flow
945
+
946
+ ```
947
+ Application.start()
948
+ |
949
+ +-- Bun.serve() / serve() <-- Server created
950
+ |
951
+ +-- executePostStartHooks() <-- Hooks run here
952
+ |
953
+ +-- SocketIOComponent hook:
954
+ 1. Get server instance via getServerInstance()
955
+ 2. Create SocketIOServerHelper with runtime-specific options
956
+ 3. Call helper.configure() to initialize Socket.IO server
957
+ 4. Bind the helper instance for injection
958
+ ```
959
+
960
+ #### Detailed Hook Timing
961
+ ```
962
+ executePostStartHooks()
963
+ |-- Hook 1: "socket-io-initialize"
964
+ | |-- performance.now() -> start
965
+ | |-- await hook()
966
+ | +-- log: "Executed hook | identifier: socket-io-initialize | took: 12.5 (ms)"
967
+ |-- Hook 2: "another-hook"
968
+ | +-- ...
969
+ +-- (hooks run sequentially in registration order)
970
+ ```
971
+
972
+ - Hooks run **sequentially** (not parallel) to guarantee ordering
973
+ - Each hook is timed with `performance.now()` for diagnostics
974
+ - If a hook throws, it propagates to `start()` and the server fails to start
975
+
976
+ #### What Happens Inside the Hook
977
+
978
+ For **Bun runtime**, the hook:
979
+
980
+ 1. Calls `createBunEngine({ serverOptions })` which dynamically imports `@socket.io/bun-engine` and creates a `BunEngine` instance with CORS config bridging
981
+ 2. Creates `SocketIOServerHelper` with `runtime: RuntimeModules.BUN` and the engine
982
+ 3. Awaits `socketIOHelper.configure()` which connects Redis pub/sub/emitter clients, initializes the `IOServer`, and sets up the Redis adapter
983
+ 4. Binds the helper to `SOCKET_IO_INSTANCE`
984
+ 5. Gets the Bun server instance and Hono server
985
+ 6. Calls `serverInstance.reload({ fetch, websocket })` to wire the engine's fetch and websocket handlers into the running Bun server, where `fetch` is the result of `createBunFetchHandler()` and `websocket` is from `engineHandler.websocket`
986
+
987
+ For **Node.js runtime**, the hook:
988
+
989
+ 1. Gets the HTTP server instance via `getServerInstance()`
990
+ 2. Validates the server instance exists (throws if not)
991
+ 3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
992
+ 4. Binds the helper to `SOCKET_IO_INSTANCE`
993
+
994
+ > [!NOTE]
995
+ > The hook identifier is `'socket-io-initialize'` for both runtimes. Only one runtime path executes per application.
996
+
997
+ ### Graceful Shutdown
998
+
999
+ Always shut down the Socket.IO server before stopping the application:
1000
+
1001
+ #### Shutdown Implementation
1002
+ ```typescript
1003
+ override async stop(): Promise<void> {
1004
+ // 1. Shut down Socket.IO (disconnects all clients, closes IO server, quits Redis)
1005
+ const socketIOHelper = this.get<SocketIOServerHelper>({
1006
+ key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
1007
+ isOptional: true,
1008
+ });
1009
+
1010
+ if (socketIOHelper) {
1011
+ await socketIOHelper.shutdown();
1012
+ }
1013
+
1014
+ // 2. Disconnect Redis helper
1015
+ if (this.redisHelper) {
1016
+ await this.redisHelper.disconnect();
1017
+ }
1018
+
1019
+ // 3. Stop the HTTP/Bun server
1020
+ await super.stop();
1021
+ }
1022
+ ```
1023
+
1024
+ #### Shutdown Flow
1025
+ ```
1026
+ socketIOHelper.shutdown()
1027
+ |-- Disconnect all tracked clients
1028
+ | |-- clearInterval(ping)
1029
+ | |-- clearTimeout(authenticateTimeout)
1030
+ | +-- socket.disconnect()
1031
+ |-- clients.clear()
1032
+ |-- io.close() -- closes the Socket.IO server (async)
1033
+ +-- Redis cleanup
1034
+ |-- redisPub.quit()
1035
+ |-- redisSub.quit()
1036
+ +-- redisEmitter.quit()
1037
+ ```
1038
+
1039
+ Client helper shutdown:
1040
+ ```
1041
+ clientHelper.shutdown()
1042
+ |-- removeAllListeners() -- prevents memory leaks
1043
+ |-- disconnect() -- if still connected
1044
+ +-- state = UNAUTHORIZED
1045
+ ```
1046
+
1047
+ ## See Also
1048
+
1049
+ - [Setup & Configuration](./) -- Quick reference, installation, bindings, constants
1050
+ - [Usage & Examples](./usage) -- Server-side usage, client helper, advanced patterns
1051
+ - [Error Reference](./errors) -- Error conditions and troubleshooting