@venizia/ignis-docs 0.2.0 → 0.2.1-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 (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,366 +1,328 @@
1
- # Socket.IO -- API Reference
1
+ ---
2
+ title: Socket.IO Component - Full Reference
3
+ description: Binding keys, configuration options, event payloads, method signatures, lifecycle diagrams, and internals
4
+ difficulty: intermediate
5
+ ---
6
+
7
+ # Socket.IO Component Reference
8
+
9
+ Every binding key, configuration option, event payload, and internal mechanism of `SocketIOComponent`, `SocketIOServerHelper`, and `SocketIOClientHelper`. For the task-oriented walkthrough, see [Usage & Examples](./usage).
10
+
11
+ **Files:**
12
+
13
+ - [`packages/core-server/src/components/socket-io/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/component.ts)
14
+ - [`packages/core-server/src/components/socket-io/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/common/keys.ts)
15
+ - [`packages/core-server/src/components/socket-io/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/common/types.ts)
16
+ - [`packages/core-server/src/components/socket-io/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/handlers/bun.handler.ts)
17
+ - [`packages/core-server/src/components/socket-io/handlers/node.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/socket-io/handlers/node.handler.ts)
18
+ - [`packages/helpers/src/modules/socket/socket-io/server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/server/helper.ts)
19
+ - [`packages/helpers/src/modules/socket/socket-io/client/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/client/helper.ts)
20
+ - [`packages/helpers/src/modules/socket/socket-io/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/socket-io/common/constants.ts)
21
+
22
+ ## Find what you need
23
+
24
+ | You want to | Go to |
25
+ |---|---|
26
+ | See every default server option | [Default server options](#default-server-options) |
27
+ | Find a binding key, its type, or whether it's required | [Binding keys](#binding-keys) |
28
+ | Look up an event name or default room | [System events and rooms](#system-events-and-rooms) |
29
+ | Understand the client authentication states | [Client states](#client-states) |
30
+ | Compare Node.js vs Bun behavior | [Runtime-specific behavior](#runtime-specific-behavior) |
31
+ | Call a method on the server helper | [Server helper: public methods](#server-helper-public-methods) |
32
+ | Send a message that reaches another server instance | [Messaging via `send()`](#messaging-via-send) |
33
+ | Call a method on the client helper | [Client helper: public methods](#client-helper-public-methods) |
34
+ | Understand the post-start hook / boot lifecycle | [Lifecycle integration](#lifecycle-integration) |
35
+ | See why one Redis connection becomes three | [Redis 3-client architecture](#redis-3-client-architecture) |
36
+ | Read the full TypeScript type definitions | [Types reference](#types-reference) |
37
+ | Look up an exact error message | [Error Reference](./errors) |
38
+
39
+ ## Quick reference
40
+
41
+ | Item | Value |
42
+ |---|---|
43
+ | Package | `@venizia/ignis` (core component) + `@venizia/ignis-helpers` (helper classes) |
44
+ | Component class | `SocketIOComponent` |
45
+ | Server helper | [`SocketIOServerHelper`](/extensions/helpers/socket-io/) |
46
+ | Client helper | [`SocketIOClientHelper`](/extensions/helpers/socket-io/) |
47
+ | Runtimes | Node.js (`@hono/node-server`) and Bun (native) |
48
+ | Scaling | `@socket.io/redis-adapter` + `@socket.io/redis-emitter` (`ioredis`) |
49
+
50
+ ## Import paths
51
+
52
+ `SocketIOComponent` and `SocketIOBindingKeys` are exported only from the `@venizia/ignis/socket-io` subpath - never from the `@venizia/ignis` root barrel.
53
+
54
+ ```typescript
55
+ // Core - subpath import only
56
+ import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
57
+
58
+ // Helpers - subpath import
59
+ import { SocketIOServerHelper, SocketIOClientHelper, SocketIOConstants } from '@venizia/ignis-helpers/socket-io';
60
+ import type { TSocketIOAuthenticateFn, TSocketIOValidateRoomFn } from '@venizia/ignis-helpers/socket-io';
61
+ ```
62
+
63
+ ## Configuration reference
64
+
65
+ ### Default server options
66
+
67
+ The component applies these defaults whenever `SocketIOBindingKeys.SERVER_OPTIONS` is unbound or only partially overridden.
68
+
69
+ | Option | Default | Description |
70
+ |---|---|---|
71
+ | `identifier` | `'SOCKET_IO_SERVER'` | Unique identifier for the helper instance |
72
+ | `path` | `'/io'` | URL path for the Socket.IO handshake and polling |
73
+ | `cors.origin` | `'*'` | Allowed origins - restrict this in production |
74
+ | `cors.methods` | `['GET', 'POST']` | Allowed HTTP methods for CORS preflight |
75
+ | `cors.preflightContinue` | `false` | Pass preflight to the next handler |
76
+ | `cors.optionsSuccessStatus` | `204` | Status code for a successful OPTIONS request |
77
+ | `cors.credentials` | `true` | Allow cookies and auth headers |
78
+ | `perMessageDeflate.threshold` | `4096` | Minimum message size to compress, in bytes |
79
+ | `perMessageDeflate.concurrencyLimit` | `20` | Max concurrent compression operations |
80
+ | `perMessageDeflate.clientNoContextTakeover` | `true` | Client releases its compression context after each message |
81
+ | `perMessageDeflate.serverNoContextTakeover` | `true` | Server releases its compression context after each message |
82
+ | `perMessageDeflate.serverMaxWindowBits` | `10` | Server-side max window size (2^10 = 1 KB) |
2
83
 
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
- +-----------------------------------+
84
+ > [!WARNING]
85
+ > The default `cors.origin: '*'` is for development only. Restrict it to your domains in production.
86
+
87
+ ```typescript
88
+ const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
89
+ identifier: 'SOCKET_IO_SERVER',
90
+ path: '/io',
91
+ cors: {
92
+ origin: '*',
93
+ methods: ['GET', 'POST'],
94
+ preflightContinue: false,
95
+ optionsSuccessStatus: 204,
96
+ credentials: true,
97
+ },
98
+ perMessageDeflate: {
99
+ threshold: 4096,
100
+ zlibDeflateOptions: { chunkSize: 10 * 1024 },
101
+ zlibInflateOptions: { windowBits: 12, memLevel: 8 },
102
+ clientNoContextTakeover: true,
103
+ serverNoContextTakeover: true,
104
+ serverMaxWindowBits: 10,
105
+ concurrencyLimit: 20,
106
+ },
107
+ };
65
108
  ```
66
109
 
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
110
+ ### Custom configuration
82
111
 
83
- #### Bun Runtime
112
+ Bind custom server options before registering the component:
84
113
 
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
114
  ```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`:
115
+ import { SocketIOBindingKeys } from '@venizia/ignis/socket-io';
116
+ import type { ServerOptions } from 'socket.io';
113
117
 
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();
118
+ const customOptions: Partial<ServerOptions> = {
119
+ path: '/socket.io',
120
+ cors: { origin: ['https://myapp.com'], methods: ['GET', 'POST'], credentials: true },
121
+ pingTimeout: 60000,
122
+ pingInterval: 25000,
123
+ maxHttpBufferSize: 1e6, // 1MB
124
+ };
157
125
 
158
- return socketIOHelper;
159
- }
126
+ this.bind<Partial<ServerOptions>>({ key: SocketIOBindingKeys.SERVER_OPTIONS }).toValue(customOptions);
127
+ this.component(SocketIOComponent);
160
128
  ```
161
129
 
162
- ## Server Helper API Reference
130
+ > [!NOTE]
131
+ > `identifier` belongs to the component's `IServerOptions` interface, which extends Socket.IO's native `ServerOptions` - it's not a Socket.IO option itself. Set it by including it in the bound options object.
163
132
 
164
- ### `SocketIOServerHelper` Constructor
133
+ ### Binding keys
165
134
 
166
- The helper uses a **discriminated union** for its constructor options, keyed on `runtime`:
135
+ | Binding key | Constant | Type | Required | Default |
136
+ |---|---|---|---|---|
137
+ | `@app/socket-io/server-options` | `SERVER_OPTIONS` | `Partial<ServerOptions>` | No | [Default server options](#default-server-options) |
138
+ | `@app/socket-io/redis-connection` | `REDIS_CONNECTION` | `IRedisHelper` (`RedisSingleHelper` / `RedisClusterHelper` / `RedisSentinelHelper`) | **Yes** | `null` |
139
+ | `@app/socket-io/authenticate-handler` | `AUTHENTICATE_HANDLER` | `TSocketIOAuthenticateFn` | **Yes** | `null` |
140
+ | `@app/socket-io/validate-room-handler` | `VALIDATE_ROOM_HANDLER` | `TSocketIOValidateRoomFn` | No | `null` |
141
+ | `@app/socket-io/client-connected-handler` | `CLIENT_CONNECTED_HANDLER` | `TSocketIOClientConnectedFn` | No | `null` |
142
+ | `@app/socket-io/instance` | `SOCKET_IO_INSTANCE` | `SocketIOServerHelper` | - | Set by the component |
167
143
 
168
- #### `TSocketIOServerOptions` Type
169
- ```typescript
170
- interface ISocketIOServerBaseOptions {
171
- identifier: string;
172
- serverOptions: Partial<ServerOptions>;
173
- redisConnection: IRedisHelper;
174
- defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
175
- authenticateTimeout?: number; // Default: 10_000 (10 seconds)
176
- pingInterval?: number; // Default: 30_000 (30 seconds)
144
+ > [!NOTE]
145
+ > `SOCKET_IO_INSTANCE` is never bound by application code. The component binds it automatically after the server starts. Inject it lazily - see [Inject the helper in a service or controller](./usage#inject-the-helper-in-a-service-or-controller).
177
146
 
178
- authenticateFn: TSocketIOAuthenticateFn;
179
- validateRoomFn?: TSocketIOValidateRoomFn;
180
- clientConnectedFn?: TSocketIOClientConnectedFn;
181
- }
147
+ ### System events and rooms
182
148
 
183
- interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
184
- runtime: typeof RuntimeModules.NODE;
185
- server: HTTPServer; // node:http.Server instance
186
- }
149
+ Exported from `@venizia/ignis-helpers/socket-io` as `SocketIOConstants`. Used internally by both the component and the helper.
187
150
 
188
- interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
189
- runtime: typeof RuntimeModules.BUN;
190
- engine: any; // @socket.io/bun-engine Server instance
191
- }
151
+ | Constant | Value | Description |
152
+ |---|---|---|
153
+ | `EVENT_PING` | `'ping'` | Keep-alive ping, emitted at `pingInterval` (default: 30s) |
154
+ | `EVENT_CONNECT` | `'connection'` | New client connected (server-side event name) |
155
+ | `EVENT_DISCONNECT` | `'disconnect'` | Client disconnected |
156
+ | `EVENT_JOIN` | `'join'` | Client requests to join room(s) |
157
+ | `EVENT_LEAVE` | `'leave'` | Client requests to leave room(s) |
158
+ | `EVENT_AUTHENTICATE` | `'authenticate'` | Client sends auth credentials |
159
+ | `EVENT_AUTHENTICATED` | `'authenticated'` | Auth success response sent to the client |
160
+ | `EVENT_UNAUTHENTICATE` | `'unauthenticated'` | Auth failure response sent to the client |
161
+ | `ROOM_DEFAULT` | `'io-default'` | Default room every authenticated client joins |
162
+ | `ROOM_NOTIFICATION` | `'io-notification'` | Notification broadcast room |
192
163
 
193
- type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
194
- ```
164
+ > [!TIP]
165
+ > Override the default rooms with the `defaultRooms` option on `SocketIOServerHelper` - the values above are only the fallback.
195
166
 
196
- During construction:
167
+ Two more constants govern default behavior. They're defined at module scope in the server helper, not exported, but overridable through constructor options:
197
168
 
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
169
+ | Constant | Value | Overridable via |
170
+ |---|---|---|
171
+ | `CLIENT_AUTHENTICATE_TIMEOUT` | `10_000` (10s) | `authenticateTimeout` |
172
+ | `CLIENT_PING_INTERVAL` | `30_000` (30s) | `pingInterval` |
202
173
 
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 `RedisSingleHelper` connection is not consumed.
174
+ ### Client states
205
175
 
206
- ### `configure()` -- Server Initialization
176
+ Each connected client tracks an authentication state that governs what it can do.
207
177
 
208
- The `configure()` method is the main initialization entry point, called after construction:
178
+ | State | Constant | Description |
179
+ |---|---|---|
180
+ | `unauthorized` | `SocketIOClientStates.UNAUTHORIZED` | Initial state - the client must emit `authenticate` within the timeout (default: 10s) |
181
+ | `authenticating` | `SocketIOClientStates.AUTHENTICATING` | Auth in progress - `authenticateFn` is running |
182
+ | `authenticated` | `SocketIOClientStates.AUTHENTICATED` | Auth succeeded - the client can send/receive events and join rooms |
209
183
 
210
184
  ```
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)
185
+ connect ----------> unauthorized --(emit 'authenticate')--> authenticating
186
+ ^ |
187
+ | success failure
188
+ timeout (10s) | |
189
+ | v v
190
+ disconnect <---------------- authenticated unauthorized -> disconnect
221
191
  ```
222
192
 
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.
193
+ ## Architecture
225
194
 
226
- ### Public Methods
195
+ ### Lifecycle integration
227
196
 
228
- #### `getIOServer()`
197
+ Socket.IO needs a running server, but components initialize before the server starts. The application's **post-start hook** system bridges that gap.
229
198
 
230
- ```typescript
231
- getIOServer(): IOServer
232
199
  ```
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
200
+ preConfigure() <- register SocketIOComponent here
201
+ |
202
+ initialize() <- component.binding() runs: resolve bindings, register post-start hook
203
+ |
204
+ setupMiddlewares()
205
+ |
206
+ startBunModule() / startNodeModule() <- server starts, instance created
207
+ |
208
+ executePostStartHooks() <- 'socket-io-initialize' hook runs:
209
+ | new SocketIOServerHelper(...)
210
+ | await socketIOHelper.configure()
211
+ | bind SOCKET_IO_INSTANCE
212
+ | (Bun only) server.reload({ fetch, websocket })
240
213
  ```
241
214
 
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()`
215
+ - Hooks run **sequentially**, in registration order, each timed with `performance.now()` for diagnostics.
216
+ - If a hook throws, the error propagates to `start()` and the server fails to start.
217
+ - The hook identifier is `'socket-io-initialize'` for both runtimes - only one runtime path executes per application.
245
218
 
246
219
  ```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()`
220
+ // Register a hook, during the binding phase
221
+ application.registerPostStartHook({
222
+ identifier: string,
223
+ hook: () => ValueOrPromise<void>,
224
+ });
255
225
 
256
- ```typescript
257
- on<HandlerArgsType extends unknown[] = unknown[], HandlerReturnType = void>(opts: {
258
- topic: string;
259
- handler: (...args: HandlerArgsType) => ValueOrPromise<HandlerReturnType>;
260
- }): void
226
+ // Get the server instance, available only after start
227
+ application.getServerInstance<T>(): T | undefined;
261
228
  ```
262
229
 
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.
230
+ ### Runtime-specific behavior
264
231
 
265
- #### `ping()`
232
+ | Aspect | Node.js | Bun |
233
+ |---|---|---|
234
+ | Server type | `node:http.Server` | `Bun.Server` |
235
+ | IO server init | `new IOServer(httpServer, opts)` | `new IOServer()` then `io.bind(engine)` |
236
+ | Engine | Built in (`socket.io`) | `@socket.io/bun-engine` (optional peer dependency) |
237
+ | Request routing | Socket.IO attaches to the HTTP server automatically | `server.reload({ fetch, websocket })` wires the engine into Bun's request loop |
238
+ | WebSocket upgrade | Handled by `node:http.Server`'s upgrade event | Handled by Bun's `websocket` handler |
239
+ | Dynamic import | None needed | `await import('@socket.io/bun-engine')` at runtime |
240
+ | Fetch handler | Not needed - the HTTP server handles upgrades | A custom fetch wraps Hono's fetch and routes WS upgrades to the engine |
241
+ | CORS | Handled by Socket.IO's own CORS options | Handled by Bun engine options, via explicit field bridging |
266
242
 
267
- ```typescript
268
- ping(opts: { socket: IOSocket; doIgnoreAuth: boolean }): void
269
- ```
243
+ ### Bun runtime details
270
244
 
271
- Sends a `ping` event to a specific client with `{ time: <ISO string> }`. Behavior:
245
+ A custom fetch function intercepts WebSocket upgrade requests before they reach Hono:
272
246
 
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
247
+ 1. Checks whether the request path starts with the Socket.IO path (`serverOptions.path`, default `'/io'`).
248
+ 2. If it matches, delegates to `@socket.io/bun-engine` via `engine.handleRequest(req, server)`.
249
+ 3. If it doesn't, delegates to Hono's normal `server.fetch(req, server)` handler.
277
250
 
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.
251
+ Socket.IO and `@socket.io/bun-engine` define CORS slightly differently, so the component extracts each field explicitly instead of casting with `as any`. See [`createBunEngine()`](#post-start-hooks) in Internals for the full CORS bridging code.
279
252
 
280
- #### `disconnect()`
253
+ ### Node.js runtime details
281
254
 
282
- ```typescript
283
- disconnect(opts: { socket: IOSocket }): void
284
- ```
255
+ Node mode is simpler - Socket.IO attaches to `node:http.Server` natively. The handler builds `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and passes the HTTP server instance directly. See [`createNodeSocketIOHelper()`](#post-start-hooks) in Internals.
285
256
 
286
- Disconnects a specific client and cleans up resources:
257
+ ## Server helper API reference
287
258
 
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
259
+ ### Constructor
292
260
 
293
- If the socket is `undefined` or not tracked in the client map, the method still calls `socket.disconnect()` for safety.
261
+ `new SocketIOServerHelper(opts: TSocketIOServerOptions)` - see [server types](#server-types) for the full discriminated union.
294
262
 
295
- #### `onClientConnect()`
263
+ | Step | What happens |
264
+ |---|---|
265
+ | 1 | Sets `identifier`, `runtime`, `serverOptions`, and the callback functions |
266
+ | 2 | Applies defaults: `authenticateTimeout` = 10s, `pingInterval` = 30s, `defaultRooms` = `['io-default', 'io-notification']` |
267
+ | 3 | `setRuntime()` validates and stores the server or engine - see [runtime validation](#setruntime-runtime-validation) |
268
+ | 4 | `initRedisClients()` creates 3 duplicated Redis clients - see [Redis 3-client architecture](#redis-3-client-architecture) |
296
269
 
297
- ```typescript
298
- onClientConnect(opts: { socket: IOSocket }): void
299
- ```
270
+ > [!IMPORTANT]
271
+ > Redis clients are duplicated from the parent connection via `redisConnection.duplicateClient()`. The helper owns 3 independent connections (pub, sub, emitter) that inherit config from the parent but keep separate state. The parent connection is never consumed.
300
272
 
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.
273
+ ### `configure()`
302
274
 
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()`
275
+ The only async method on the helper. It waits for all 3 Redis clients to reach `ready` before building the IO server.
310
276
 
311
- #### `onClientAuthenticated()`
277
+ 1. Registers an `error` handler on each of the 3 Redis clients.
278
+ 2. Connects any client still in `wait` status (lazy-connect mode).
279
+ 3. Awaits all 3 clients reaching `ready`.
280
+ 4. Builds the IO server - `new IOServer(httpServer, serverOptions)` on Node.js, or `new IOServer()` + `io.bind(bunEngine)` on Bun.
281
+ 5. Wires the Redis adapter: `io.adapter(createAdapter(redisPub, redisSub))`.
282
+ 6. Creates the Redis emitter: `emitter = new Emitter(redisEmitter)`.
283
+ 7. Registers the `connection` handler, which calls `onClientConnect()` for every new socket.
312
284
 
313
- ```typescript
314
- onClientAuthenticated(opts: { socket: IOSocket }): void
315
- ```
285
+ > [!NOTE]
286
+ > If any Redis client fails to connect, the error propagates and the server does not start.
316
287
 
317
- Called after successful authentication. This method is public so it can be invoked externally for testing or custom auth flows.
288
+ ### Server helper: public methods
318
289
 
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)
290
+ | Method | Signature | Behavior |
291
+ |---|---|---|
292
+ | `getIOServer()` | `(): IOServer` | Returns the underlying `socket.io` `Server` - for APIs the helper doesn't expose, like `io.of()` or `io.fetchSockets()` |
293
+ | `getEngine()` | `(): any` | Returns the `@socket.io/bun-engine` instance. Throws on Node.js runtime |
294
+ | `getClients()` | `(opts?: { id? }): ISocketIOClient \| Map<string, ISocketIOClient> \| undefined` | Without `id`, returns the full client map. With `id`, returns that client or `undefined` |
295
+ | `on()` | `(opts: { topic; handler }): void` | Registers a server-level event handler. Throws if `topic` is empty, `handler` is falsy, or the IO server isn't initialized |
296
+ | `ping()` | `(opts: { socket; doIgnoreAuth }): void` | Sends `{ time }` to one client. No-op if `socket` or the client entry is missing. Disconnects the client if `doIgnoreAuth` is `false` and it isn't authenticated |
297
+ | `disconnect()` | `(opts: { socket }): void` | Clears the client's ping interval and auth timeout, removes it from the client map, then calls `socket.disconnect()`. No-op if `socket` is falsy |
298
+ | `onClientConnect()` | `(opts: { socket }): void` | Runs on every new connection: skips duplicates, creates the client entry (state `UNAUTHORIZED`), starts the auth timeout, and registers the `disconnect` and `authenticate` handlers |
299
+ | `onClientAuthenticated()` | `(opts: { socket }): void` | Runs after successful auth: sets state `AUTHENTICATED`, sends the initial ping, joins default rooms, registers room handlers, starts the ping interval, and emits `authenticated` |
328
300
 
329
- ### Messaging via `send()`
301
+ - **`ping()` powers the keep-alive interval.** `doIgnoreAuth: true` covers both the initial post-auth ping and the recurring interval.
302
+ - **`onClientConnect()` and `onClientAuthenticated()` are public** so tests and custom connection routing can call them directly.
303
+ - **`clientConnectedFn` runs through a safety wrapper.** A synchronous throw inside your callback is caught and logged - it never crashes the process.
330
304
 
331
- The `send()` method uses the Redis emitter for message delivery, enabling cross-instance broadcasting:
305
+ ### Messaging via `send()`
332
306
 
333
307
  ```typescript
334
308
  send(opts: {
335
309
  destination?: string; // Socket ID, room name, or omit for broadcast
336
- payload: {
337
- topic: string; // Event name
338
- data: any; // Event payload
339
- };
310
+ payload: { topic: string; data: any };
340
311
  doLog?: boolean; // Log the emission (default: false)
341
- cb?: () => void; // Callback executed via setImmediate after emit
312
+ callback?: () => void; // Executed via setImmediate after emit
342
313
  })
343
314
  ```
344
315
 
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
316
+ `send()` delivers through the Redis emitter, so it works even when the destination client is connected to a different server instance.
354
317
 
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
318
+ | `destination` | Behavior |
319
+ |---|---|
320
+ | A socket ID or room name | `sender.to(destination).emit(topic, data)` |
321
+ | Omitted or empty | Broadcasts to all connected clients: `sender.emit(topic, data)` |
359
322
 
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.
323
+ Every message is compressed via `emitter.compress(true)`. `callback` runs through `setImmediate()` - it confirms the emit call ran, not that a client received the message. Logging is opt-in (`doLog: true`) to avoid noise at high throughput.
361
324
 
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.
325
+ `send()` returns silently, with no error and no log, when `payload`, `payload.topic`, or `payload.data` is falsy. That's deliberate: a fire-and-forget caller doesn't need to know a message was dropped for a missing field.
364
326
 
365
327
  ### Shutdown
366
328
 
@@ -368,17 +330,14 @@ This is a deliberate design choice for fire-and-forget messaging patterns where
368
330
  shutdown(): Promise<void>
369
331
  ```
370
332
 
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`)
333
+ 1. Clears every tracked client's ping interval and auth timeout, then disconnects each socket.
334
+ 2. Clears the client map.
335
+ 3. Closes the IO server (`io.close()`, wrapped in a promise).
336
+ 4. Quits all 3 Redis connections (`redisPub`, `redisSub`, `redisEmitter`).
378
337
 
379
- ## Client Helper API Reference
338
+ ## Client helper API reference
380
339
 
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.
340
+ `SocketIOClientHelper` extends `BaseHelper` and wraps `socket.io-client` with lifecycle callbacks, error-safe event subscription, and authentication state tracking.
382
341
 
383
342
  ### Constructor
384
343
 
@@ -386,121 +345,59 @@ Gracefully shuts down the server:
386
345
  constructor(opts: ISocketIOClientOptions)
387
346
  ```
388
347
 
389
- #### `ISocketIOClientOptions` Interface
348
+ See [client types](#client-types) for the full interface. `IOptions` extends `socket.io-client`'s `SocketOptions` with two required fields:
390
349
 
391
- ```typescript
392
- interface ISocketIOClientOptions {
393
- identifier: string;
394
- host: string;
395
- options: IOptions;
350
+ | Field | Purpose |
351
+ |---|---|
352
+ | `path` | Must match the server's `path` |
353
+ | `extraHeaders` | Commonly used for the `authorization` token |
396
354
 
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
- ```
355
+ Construction, in order:
405
356
 
406
- #### `IOptions` Interface
357
+ 1. Calls `super({ scope: opts.identifier })` to set up `BaseHelper` with scoped logging.
358
+ 2. Stores `identifier`, `host`, `options`, and every lifecycle callback.
359
+ 3. Calls `configure()` immediately to create the socket and register handlers.
407
360
 
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()`
361
+ ### Client `configure()` event handlers
426
362
 
427
363
  ```typescript
428
364
  configure(): void
429
365
  ```
430
366
 
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`).
367
+ Creates the `socket.io-client` `Socket` instance and registers every internal handler. If `configure()` already ran, it logs a message and returns early.
468
368
 
469
- ### `authenticate()`
369
+ | Event | Internal behavior |
370
+ |---|---|
371
+ | `connect` | Logs the connection, invokes `onConnected` |
372
+ | `disconnect` | Logs the disconnection with a reason, resets state to `unauthorized`, invokes `onDisconnected` |
373
+ | `connect_error` | Logs the error, invokes `onError` |
374
+ | `authenticated` | Logs the auth data, sets state to `authenticated`, invokes `onAuthenticated` |
375
+ | `unauthenticated` | Logs a warning with the auth data, resets state to `unauthorized`, invokes `onUnauthenticated` with the message |
376
+ | `ping` | Logs a debug-level "ping received" |
470
377
 
471
- ```typescript
472
- authenticate(): void
473
- ```
378
+ Every lifecycle callback runs inside `Promise.resolve(...).catch(...)`, so a callback error never crashes the client.
474
379
 
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`.
380
+ ### Client helper: public methods
476
381
 
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)
382
+ | Method | Signature | Behavior |
383
+ |---|---|---|
384
+ | `getState()` | `(): TSocketIOClientState` | Returns `'unauthorized'`, `'authenticating'`, or `'authenticated'` |
385
+ | `getSocketClient()` | `(): Socket` | Returns the raw `socket.io-client` `Socket` - for APIs the helper doesn't expose, like `socket.id` |
386
+ | `authenticate()` | `(): void` | Emits `authenticate`. No-op with a warning log unless connected and in state `unauthorized` |
387
+ | `subscribe()` | `<T>(opts: { event; handler; ignoreDuplicate? }): void` | Subscribes with error-safe wrapping - see [below](#subscribe-error-safe-wrapping) |
388
+ | `subscribeMany()` | `(opts: { events; ignoreDuplicate? }): void` | Calls `subscribe()` for every entry in `events` |
389
+ | `unsubscribe()` | `(opts: { event; handler? }): void` | Removes one handler, or all handlers for the event if `handler` is omitted |
390
+ | `unsubscribeMany()` | `(opts: { events: string[] }): void` | Calls `unsubscribe()` for every event in the array |
391
+ | `connect()` | `(): void` | Manually connects. No-op with an info log if the client isn't initialized |
392
+ | `disconnect()` | `(): void` | Manually disconnects. No-op with an info log if the client isn't initialized |
393
+ | `emit()` | `<T>(opts: { topic; data; doLog?; callback? }): void` | Emits an event. Throws if not connected or `topic` is falsy - see [throw conditions](#emit-throw-conditions) |
394
+ | `joinRooms()` | `(opts: { rooms: string[] }): void` | Emits `join` with `{ rooms }`. No-op with a warning log if not connected |
395
+ | `leaveRooms()` | `(opts: { rooms: string[] }): void` | Emits `leave` with `{ rooms }`. No-op with a warning log if not connected |
396
+ | `shutdown()` | `(): void` | Removes all listeners, disconnects if connected, resets state to `unauthorized` |
480
397
 
481
- On call:
482
- 1. Sets state to `authenticating`
483
- 2. Emits `SocketIOConstants.EVENT_AUTHENTICATE` (value: `'authenticate'`)
398
+ #### `subscribe()` error-safe wrapping
484
399
 
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:
400
+ Every handler is wrapped in a dual try-catch, so a broken handler never crashes the client:
504
401
 
505
402
  ```typescript
506
403
  const wrappedHandler = (data: T) => {
@@ -514,337 +411,234 @@ const wrappedHandler = (data: T) => {
514
411
  };
515
412
  ```
516
413
 
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
- ```
414
+ The outer `try-catch` handles synchronous throws. The `.catch()` on `Promise.resolve()` handles async rejections.
524
415
 
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
- ```
416
+ #### `emit()` throw conditions
543
417
 
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`
418
+ | Condition | statusCode | Message |
419
+ |---|---|---|
420
+ | Socket not connected | `400` | `"Invalid socket client state to emit"` |
421
+ | `topic` is falsy | `400` | `"Topic is required to emit"` |
622
422
 
623
423
  ## Internals
624
424
 
625
425
  ### `resolveBindings()`
626
426
 
627
- Reads all binding keys from the DI container and validates required ones:
427
+ Reads every binding key from the DI container and validates the required ones.
628
428
 
629
- | Binding | Validation | Error on Failure |
630
- |---------|-----------|------------------|
631
- | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
632
- | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisSingleHelper (single), RedisClusterHelper (cluster), or RedisSentinelHelper (sentinel)"` |
633
- | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"[DANGER][SocketIOComponent] 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` | -- |
429
+ | Binding | Validation | Error on failure |
430
+ |---|---|---|
431
+ | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | - |
432
+ | `REDIS_CONNECTION` | Must pass `isRedisHelper()` | `"Invalid instance of redisConnection..."` |
433
+ | `AUTHENTICATE_HANDLER` | Must be a truthy function | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
434
+ | `VALIDATE_ROOM_HANDLER` | Optional, resolved from the container, `null` coerced to `undefined` | - |
435
+ | `CLIENT_CONNECTED_HANDLER` | Optional, resolved from the container, `null` coerced to `undefined` | - |
636
436
 
637
- ### `registerBunHook()`
437
+ ### Post-start hooks
638
438
 
639
- Registers a post-start hook that:
439
+ Both runtimes register the same hook identifier, `'socket-io-initialize'`, but wire the helper differently.
640
440
 
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
441
+ **Bun** (`registerBunHook()`):
646
442
 
647
- ### `createBunEngine()` Function
443
+ 1. Calls `createBunEngine({ serverOptions })`, which dynamically imports `@socket.io/bun-engine` and builds a `BunEngine` instance with CORS bridging.
444
+ 2. Constructs `SocketIOServerHelper` with `runtime: RuntimeModules.BUN`.
445
+ 3. Awaits `socketIOHelper.configure()`.
446
+ 4. Binds the helper to `SOCKET_IO_INSTANCE`.
447
+ 5. Calls `serverInstance.reload({ fetch, websocket })` to wire the engine into the running Bun server.
648
448
 
649
449
  ```typescript
650
450
  async function createBunEngine(opts: {
651
451
  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
452
+ }): Promise<{ engine: any; engineHandler: any }> {
453
+ const { serverOptions } = opts;
454
+ const { Server: BunEngine } = await import('@socket.io/bun-engine');
455
+
456
+ const corsConfig = typeof serverOptions.cors === 'object' ? serverOptions.cors : undefined;
457
+ const engine = new BunEngine({
458
+ path: serverOptions.path ?? '/socket.io/',
459
+ ...(corsConfig && {
460
+ cors: {
461
+ origin: corsConfig.origin as string | RegExp | (string | RegExp)[] | undefined,
462
+ methods: corsConfig.methods,
463
+ credentials: corsConfig.credentials,
464
+ allowedHeaders: corsConfig.allowedHeaders,
465
+ exposedHeaders: corsConfig.exposedHeaders,
466
+ maxAge: corsConfig.maxAge,
467
+ },
468
+ }),
469
+ });
658
470
 
659
- ```typescript
660
- function createBunFetchHandler(opts: {
661
- engine: any;
662
- enginePath: string;
663
- honoServer: OpenAPIHono;
664
- }): (req: Request, server: TBunServerInstance) => Response | Promise<Response>
471
+ return { engine, engineHandler: engine.handler() };
472
+ }
665
473
  ```
666
474
 
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:
475
+ **Node.js** (`registerNodeHook()`):
674
476
 
675
- 1. Gets the HTTP server instance via `getServerInstance()`
676
- 2. Validates the server instance exists (throws `"[SocketIOComponent] 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`
477
+ 1. Gets the HTTP server via `getServerInstance()`. Throws `"[SocketIOComponent] HTTP server not available for Node.js runtime!"` if it's missing.
478
+ 2. Calls `createNodeSocketIOHelper()`, which constructs `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`.
479
+ 3. Binds the helper to `SOCKET_IO_INSTANCE`.
679
480
 
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 `AbstractRedisHelper` instance:
481
+ ### Redis 3-client architecture
685
482
 
686
483
  ```
687
- RedisSingleHelper (parent -- NOT consumed)
484
+ RedisSingleHelper (parent - NOT consumed)
688
485
  |
689
- +-- client.duplicate() --> redisPub (for Redis adapter -- publishes)
486
+ +-- duplicateClient() --> redisPub (Redis adapter - publishes)
690
487
  |
691
- +-- client.duplicate() --> redisSub (for Redis adapter -- subscribes)
488
+ +-- duplicateClient() --> redisSub (Redis adapter - subscribes)
692
489
  |
693
- +-- client.duplicate() --> redisEmitter (for @socket.io/redis-emitter -- message delivery)
490
+ +-- duplicateClient() --> redisEmitter (Redis emitter - message delivery)
694
491
  ```
695
492
 
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 `RedisSingleHelper` connection remains independent and is not consumed -- it can be used for other purposes (e.g., caching, sessions)
493
+ Three clients, not one, for two reasons:
494
+
495
+ - **`@socket.io/redis-adapter` needs separate pub and sub clients.** A Redis connection in subscribe mode can't run other commands.
496
+ - **`@socket.io/redis-emitter` needs its own client.** It emits messages independently of the adapter, so a process with no local Socket.IO server can still broadcast.
497
+
498
+ The parent connection stays independent and unconsumed - reuse it for caching, sessions, or anything else.
700
499
 
701
- **`TRedisClient` type:**
702
500
  ```typescript
703
501
  type TRedisClient = Redis | Cluster;
704
502
  ```
705
503
 
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
504
+ This alias covers both single-instance and `Cluster` connections from `ioredis`, so the helper is transparent to the Redis deployment topology.
709
505
 
710
- The private `setRuntime()` method validates the constructor options based on the `runtime` discriminant:
506
+ ### `setRuntime()` runtime validation
711
507
 
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!"` |
508
+ | Runtime | Required field | Error on missing |
509
+ |---|---|---|
510
+ | `RuntimeModules.NODE` | `opts.server` (`HTTPServer`) | `"[SocketIOServerHelper] Invalid HTTP server for Node.js runtime!"` |
511
+ | `RuntimeModules.BUN` | `opts.engine` (`BunEngine`) | `"[SocketIOServerHelper] Invalid @socket.io/bun-engine instance for Bun runtime!"` |
512
+ | Other | - | `"[SocketIOServerHelper] Unsupported runtime!"` |
717
513
 
718
- ### `initRedisClients()` -- Redis Initialization
514
+ ### `initRedisClients()`
719
515
 
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!"`
516
+ Creates the 3 duplicated clients from the parent connection. Throws `"Invalid redis connection to config socket.io adapter!"` if `redisConnection` is falsy.
725
517
 
726
- Creates 3 duplicated clients from the parent connection's underlying ioredis client.
518
+ ### `initIOServer()`
727
519
 
728
- ### `initIOServer()` -- IO Server Initialization
729
-
730
- Called during `configure()` after Redis connections are ready:
520
+ Called during `configure()`, after the Redis connections are ready.
731
521
 
732
522
  | Runtime | Initialization |
733
- |---------|---------------|
523
+ |---|---|
734
524
  | `RuntimeModules.NODE` | `this.io = new IOServer(this.server, this.serverOptions)` |
735
525
  | `RuntimeModules.BUN` | `this.io = new IOServer()` then `this.io.bind(this.bunEngine)` |
736
526
  | Other | Throws `"Unsupported runtime: <runtime>"` |
737
527
 
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!"`
528
+ Two more guards run inside each branch:
741
529
 
742
- ### Connection Lifecycle
530
+ | Runtime | Missing field | Error |
531
+ |---|---|---|
532
+ | Node.js | `this.server` | `"[DANGER] Invalid HTTP server instance to init Socket.io server!"` |
533
+ | Bun | `this.bunEngine` | `"[DANGER] Invalid @socket.io/bun-engine instance to init Socket.io server!"` |
743
534
 
744
- When a client connects, the server manages a strict authentication flow:
535
+ ### Connection lifecycle
745
536
 
746
537
  ```
747
538
  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
- ```
539
+ -> onClientConnect(): validate socket, create client (state UNAUTHORIZED),
540
+ start authenticateTimeout (10s default), register 'disconnect' + 'authenticate' handlers
772
541
 
773
- #### Authentication Failure -- Two Code Paths
542
+ Client emits 'authenticate'
543
+ -> validate client exists and state is UNAUTHORIZED, set state AUTHENTICATING
544
+ -> call authenticateFn(handshake)
545
+ success -> onClientAuthenticated(): state AUTHENTICATED, send initial ping,
546
+ join default rooms, register 'join'/'leave' handlers,
547
+ start ping interval, emit 'authenticated', call clientConnectedFn()
548
+ failure -> emit 'unauthenticated', disconnect
774
549
 
775
- The `registerAuthHandler()` method handles authentication results through two distinct code paths:
550
+ Timeout (10s) -> disconnect if not yet AUTHENTICATED
551
+ ```
776
552
 
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)
553
+ #### Authentication failure paths
782
554
 
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
555
+ `registerAuthHandler()` resolves an auth failure through one of two paths. Both check `this.clients.has(id)` first, in case the client disconnected mid-authentication.
789
556
 
790
- Both paths also handle the edge case where the client disconnected *during* authentication -- they check `this.clients.has(id)` before proceeding.
557
+ | Path | Trigger | Actions |
558
+ |---|---|---|
559
+ | Rejected | `authenticateFn` resolves `false` | Reset state to `UNAUTHORIZED`. Send `unauthenticated` with `"Invalid token to authenticate! Please login again!"`. Disconnect after send (`setImmediate`). No error logged - this is an expected outcome. |
560
+ | Threw | `authenticateFn` throws | Reset state to `UNAUTHORIZED`. Log the error. Send `unauthenticated` with `"Failed to authenticate connection! Please login again!"`. Disconnect after send (`setImmediate`). |
791
561
 
792
- #### `ISocketIOClient` Interface
793
562
  ```typescript
794
563
  interface ISocketIOClient {
795
564
  id: string;
796
565
  socket: IOSocket;
797
566
  state: TSocketIOClientState; // 'unauthorized' | 'authenticating' | 'authenticated'
798
- interval?: NodeJS.Timeout; // Ping interval (set after auth)
799
- authenticateTimeout: NodeJS.Timeout; // Auth deadline (cleared on success)
567
+ interval?: NodeJS.Timeout; // Ping interval, set after auth
568
+ authenticateTimeout?: NodeJS.Timeout; // Auth deadline, undefined once cleared
800
569
  }
801
570
  ```
802
571
 
803
- ### Room Handlers
804
-
805
- Room join/leave handlers are registered after successful authentication:
572
+ ### Room handlers
806
573
 
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.
574
+ Registered after successful authentication.
809
575
 
810
- Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. Empty `rooms` arrays are silently ignored.
576
+ | Handler | Behavior |
577
+ |---|---|
578
+ | `join` | Client sends `{ rooms }`. If `validateRoomFn` is bound, only the rooms it returns get joined. If it isn't bound, the join is rejected with a warning log. |
579
+ | `leave` | Client sends `{ rooms }`. Always allowed - no validation function needed. |
811
580
 
812
- Join handler validation errors are caught and logged but do not disconnect the client.
581
+ Both handlers parse the payload defensively: `const { rooms = [] } = payload || { rooms: [] }`. An empty array is silently ignored. A join error is caught and logged - it never disconnects the client.
813
582
 
814
583
  > [!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.
584
+ > Without a `validateRoomFn` bound, clients **cannot** join any custom room - they stay in the default rooms only. This is security-by-default.
816
585
 
817
- ## Types Reference
586
+ ### Graceful shutdown
818
587
 
819
- ### Server Types
588
+ Shut down Socket.IO before stopping the application:
820
589
 
821
590
  ```typescript
822
- // Server constructor options -- discriminated union on 'runtime'
591
+ override async stop(): Promise<void> {
592
+ // 1. Shut down Socket.IO (disconnects all clients, closes the IO server, quits Redis)
593
+ const socketIOHelper = this.get<SocketIOServerHelper>({
594
+ key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
595
+ isOptional: true,
596
+ });
597
+ if (socketIOHelper) {
598
+ await socketIOHelper.shutdown();
599
+ }
600
+
601
+ // 2. Disconnect the Redis helper
602
+ if (this.redisHelper) {
603
+ await this.redisHelper.disconnect();
604
+ }
605
+
606
+ // 3. Stop the HTTP/Bun server
607
+ await super.stop();
608
+ }
609
+ ```
610
+
611
+ `socketIOHelper.shutdown()` runs the 4 steps under [Shutdown](#shutdown) above. `clientHelper.shutdown()` does the client-side equivalent: remove listeners, disconnect if connected, reset state.
612
+
613
+ ## Types reference
614
+
615
+ ### Server types
616
+
617
+ ```typescript
618
+ // Server constructor options - discriminated union on 'runtime'
823
619
  type TSocketIOServerOptions = ISocketIOServerNodeOptions | ISocketIOServerBunOptions;
824
620
 
825
- // Base options shared by both runtimes
826
621
  interface ISocketIOServerBaseOptions {
827
622
  identifier: string;
828
623
  serverOptions: Partial<ServerOptions>;
829
624
  redisConnection: IRedisHelper;
830
- defaultRooms?: string[];
831
- authenticateTimeout?: number;
832
- pingInterval?: number;
625
+ defaultRooms?: string[]; // Default: ['io-default', 'io-notification']
626
+ authenticateTimeout?: number; // Default: 10_000 (10 seconds)
627
+ pingInterval?: number; // Default: 30_000 (30 seconds)
628
+
833
629
  authenticateFn: TSocketIOAuthenticateFn;
834
630
  validateRoomFn?: TSocketIOValidateRoomFn;
835
631
  clientConnectedFn?: TSocketIOClientConnectedFn;
836
632
  }
837
633
 
838
- // Node.js runtime variant
839
634
  interface ISocketIOServerNodeOptions extends ISocketIOServerBaseOptions {
840
635
  runtime: typeof RuntimeModules.NODE;
841
- server: HTTPServer;
636
+ server: HTTPServer; // node:http.Server instance
842
637
  }
843
638
 
844
- // Bun runtime variant
845
639
  interface ISocketIOServerBunOptions extends ISocketIOServerBaseOptions {
846
640
  runtime: typeof RuntimeModules.BUN;
847
- engine: any;
641
+ engine: any; // @socket.io/bun-engine Server instance
848
642
  }
849
643
 
850
644
  // Tracked client entry (server-side)
@@ -853,14 +647,13 @@ interface ISocketIOClient {
853
647
  socket: IOSocket;
854
648
  state: TSocketIOClientState;
855
649
  interval?: NodeJS.Timeout;
856
- authenticateTimeout: NodeJS.Timeout;
650
+ authenticateTimeout?: NodeJS.Timeout;
857
651
  }
858
652
 
859
- // Redis client type alias
860
653
  type TRedisClient = Redis | Cluster;
861
654
  ```
862
655
 
863
- ### Client Types
656
+ ### Client types
864
657
 
865
658
  ```typescript
866
659
  // Client constructor options
@@ -868,6 +661,8 @@ interface ISocketIOClientOptions {
868
661
  identifier: string;
869
662
  host: string;
870
663
  options: IOptions;
664
+
665
+ // Lifecycle callbacks (all optional)
871
666
  onConnected?: () => ValueOrPromise<void>;
872
667
  onDisconnected?: (reason: string) => ValueOrPromise<void>;
873
668
  onError?: (error: Error) => ValueOrPromise<void>;
@@ -881,39 +676,43 @@ interface IOptions extends SocketOptions {
881
676
  extraHeaders: Record<string | symbol | number, any>;
882
677
  }
883
678
 
884
- // Event handler type (supports sync and async)
885
679
  type TSocketIOEventHandler<T = unknown> = (data: T) => ValueOrPromise<void>;
886
680
 
887
- // Client state type
888
681
  type TSocketIOClientState = TConstValue<typeof SocketIOClientStates>;
889
682
  // Resolves to: 'unauthorized' | 'authenticating' | 'authenticated'
890
683
  ```
891
684
 
892
- ### Callback Types
685
+ ### Callback types
893
686
 
894
687
  ```typescript
895
- // Server authentication handler
688
+ // Handshake payload passed to the authenticate handler
689
+ interface IHandshake {
690
+ headers: IncomingHttpHeaders;
691
+ time: string;
692
+ address: string;
693
+ xdomain: boolean;
694
+ secure: boolean;
695
+ issued: number;
696
+ url: string;
697
+ query: ParsedUrlQuery;
698
+ auth: { [key: string]: any };
699
+ }
700
+
896
701
  type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;
897
702
 
898
- // Server room validation handler
899
703
  type TSocketIOValidateRoomFn = (opts: {
900
704
  socket: IOSocket;
901
705
  rooms: string[];
902
706
  }) => ValueOrPromise<string[]>;
903
707
 
904
- // Server client connected handler
905
708
  type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;
906
- ```
907
-
908
- ### Component Types
909
709
 
910
- ```typescript
911
710
  // Extended ServerOptions with identifier
912
711
  interface IServerOptions extends ServerOptions {
913
712
  identifier: string;
914
713
  }
915
714
 
916
- // Resolved binding values from DI container
715
+ // Resolved binding values from the DI container
917
716
  interface IResolvedBindings {
918
717
  redisConnection: IRedisHelper;
919
718
  authenticateFn: TSocketIOAuthenticateFn;
@@ -922,130 +721,8 @@ interface IResolvedBindings {
922
721
  }
923
722
  ```
924
723
 
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
724
+ ## See also
1048
725
 
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
726
+ - [Overview](./) - quick start, imports, common configuration tasks
727
+ - [Usage & Examples](./usage) - full setup steps, server-side usage, client helper, advanced patterns
728
+ - [Error Reference](./errors) - error conditions and troubleshooting